btpyparse.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """ Pyparsing parser for BibTeX files
  2. A standalone parser using pyparsing.
  3. pyparsing has a simple and expressive syntax so the grammar is easy to read and
  4. write.
  5. Submitted by Matthew Brett, 2010
  6. Simplified BSD license
  7. """
  8. from pyparsing import (Regex, Suppress, ZeroOrMore, Group, Optional, Forward,
  9. SkipTo, CaselessLiteral, Dict)
  10. class Macro(object):
  11. """ Class to encapsulate undefined macro references """
  12. def __init__(self, name):
  13. self.name = name
  14. def __repr__(self):
  15. return 'Macro("%s")' % self.name
  16. def __eq__(self, other):
  17. return self.name == other.name
  18. def __ne__(self, other):
  19. return self.name != other.name
  20. # Character literals
  21. LCURLY,RCURLY,LPAREN,RPAREN,QUOTE,COMMA,AT,EQUALS,HASH = map(Suppress,'{}()",@=#')
  22. def bracketed(expr):
  23. """ Return matcher for `expr` between curly brackets or parentheses """
  24. return (LPAREN + expr + RPAREN) | (LCURLY + expr + RCURLY)
  25. # Define parser components for strings (the hard bit)
  26. chars_no_curly = Regex(r"[^{}]+")
  27. chars_no_curly.leaveWhitespace()
  28. chars_no_quotecurly = Regex(r'[^"{}]+')
  29. chars_no_quotecurly.leaveWhitespace()
  30. # Curly string is some stuff without curlies, or nested curly sequences
  31. curly_string = Forward()
  32. curly_item = Group(curly_string) | chars_no_curly
  33. curly_string << LCURLY + ZeroOrMore(curly_item) + RCURLY
  34. # quoted string is either just stuff within quotes, or stuff within quotes, within
  35. # which there is nested curliness
  36. quoted_item = Group(curly_string) | chars_no_quotecurly
  37. quoted_string = QUOTE + ZeroOrMore(quoted_item) + QUOTE
  38. # Numbers can just be numbers. Only integers though.
  39. number = Regex('[0-9]+')
  40. # Basis characters (by exclusion) for variable / field names. The following
  41. # list of characters is from the btparse documentation
  42. any_name = Regex('[^\\s"#%\'(),={}]+')
  43. # btparse says, and the test bibs show by experiment, that macro and field names
  44. # cannot start with a digit. In fact entry type names cannot start with a digit
  45. # either (see tests/bibs). Cite keys can start with a digit
  46. not_digname = Regex('[^\\d\\s"#%\'(),={}][^\\s"#%\'(),={}]*')
  47. # Comment comments out to end of line
  48. comment = (AT + CaselessLiteral('comment') +
  49. Regex(r"[\s{(].*").leaveWhitespace())
  50. # The name types with their digiteyness
  51. not_dig_lower = not_digname.copy().setParseAction(lambda t: t[0].lower())
  52. macro_def = not_dig_lower.copy()
  53. macro_ref = not_dig_lower.copy().setParseAction(lambda t : Macro(t[0].lower()))
  54. field_name = not_dig_lower.copy()
  55. # Spaces in names mean they cannot clash with field names
  56. entry_type = not_dig_lower('entry_type')
  57. cite_key = any_name('cite_key')
  58. # Number has to be before macro name
  59. string = (number | macro_ref | quoted_string | curly_string)
  60. # There can be hash concatenation
  61. field_value = string + ZeroOrMore(HASH + string)
  62. field_def = Group(field_name + EQUALS + field_value)
  63. entry_contents = Dict(ZeroOrMore(field_def + COMMA) + Optional(field_def))
  64. # Entry is surrounded either by parentheses or curlies
  65. entry = (AT + entry_type + bracketed(cite_key + COMMA + entry_contents))
  66. # Preamble is a macro-like thing with no name
  67. preamble = AT + CaselessLiteral('preamble') + bracketed(field_value)
  68. # Macros (aka strings)
  69. macro_contents = macro_def + EQUALS + field_value
  70. macro = AT + CaselessLiteral('string') + bracketed(macro_contents)
  71. # Implicit comments
  72. icomment = SkipTo('@').setParseAction(lambda t : t.insert(0, 'icomment'))
  73. # entries are last in the list (other than the fallback) because they have
  74. # arbitrary start patterns that would match comments, preamble or macro
  75. definitions = Group(comment |
  76. preamble |
  77. macro |
  78. entry |
  79. icomment)
  80. # Start symbol
  81. bibfile = ZeroOrMore(definitions)
  82. def parse_str(str):
  83. return bibfile.parseString(str)
  84. if __name__ == '__main__':
  85. # Run basic test
  86. txt = """
  87. Some introductory text
  88. (implicit comment)
  89. @ARTICLE{Authors2011,
  90. author = {First Author and Second Author and Third Author},
  91. title = {An article about {S}omething},
  92. journal = "Journal of Articles",
  93. year = {2011},
  94. volume = {16},
  95. pages = {1140--1141},
  96. number = {2}
  97. }
  98. """
  99. print('\n\n'.join(defn.dump() for defn in parse_str(txt)))