diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/Makefile b/Makefile deleted file mode 100644 index 3cea9ec..0000000 --- a/Makefile +++ /dev/null @@ -1,82 +0,0 @@ -VERSION=0.6.1 -RELEASE=1 -PACKAGE=crosstex-${VERSION} - -ROOT= -PREFIX=/usr/local -LIBDIR=/share/crosstex -BINDIR=/bin -MANDIR=/share/man -PLY=${PREFIX}${LIBDIR} - -all: - @echo nothing to make, try make install - -install: - mkdir -p $(ROOT)$(PREFIX)$(BINDIR) $(ROOT)$(PREFIX)$(LIBDIR)/crosstex/style $(ROOT)$(PREFIX)$(MANDIR)/man1 - install -m 0644 lib/crosstex/*.py $(ROOT)$(PREFIX)$(LIBDIR)/crosstex - install -m 0644 lib/crosstex/style/*.py $(ROOT)$(PREFIX)$(LIBDIR)/crosstex/style - install -m 0644 data/*.xtx $(ROOT)$(PREFIX)$(LIBDIR) - install -m 0644 crosstex.1 $(ROOT)$(PREFIX)$(MANDIR)/man1 - ln -sf crosstex.1 $(ROOT)$(PREFIX)$(MANDIR)/man1/xtx2bib.1 - ln -sf crosstex.1 $(ROOT)$(PREFIX)$(MANDIR)/man1/xtx2html.1 - ln -sf crosstex.1 $(ROOT)$(PREFIX)$(MANDIR)/man1/bib2xtx.1 - echo '/^version = /c\' >crosstex.sed - echo "version = '${VERSION}'" >>crosstex.sed - echo '/^xtxlib = /c\' >>crosstex.sed - echo "xtxlib = '${PREFIX}${LIBDIR}'" >>crosstex.sed - echo '/^plylib = /c\' >>crosstex.sed - echo "plylib = '${PLY}'" >>crosstex.sed - sed -f crosstex.sed $(ROOT)$(PREFIX)$(BINDIR)/crosstex - rm -f crosstex.sed - chmod 0755 $(ROOT)$(PREFIX)$(BINDIR)/crosstex - ln -sf crosstex $(ROOT)$(PREFIX)$(BINDIR)/xtx2bib - ln -sf crosstex $(ROOT)$(PREFIX)$(BINDIR)/xtx2html - ln -sf crosstex $(ROOT)$(PREFIX)$(BINDIR)/bib2xtx - -crosstex.pdf: crosstex.tex - pdflatex crosstex && pdflatex crosstex - -.PHONY: pdf -pdf: crosstex.pdf - -${PACKAGE}.tar.gz: COPYING Makefile crosstex crosstex.1 crosstex.pdf crosstex.spec crosstex.tex - rm -rf ${PACKAGE} - mkdir -p ${PACKAGE}/data ${PACKAGE}/debian ${PACKAGE}/lib/crosstex/style ${PACKAGE}/tests - install -m 0644 COPYING Makefile crosstex crosstex.1 crosstex.pdf crosstex.tex ${PACKAGE} - install -m 0644 data/*.xtx ${PACKAGE}/data - install -m 0644 lib/crosstex/*.py ${PACKAGE}/lib/crosstex - install -m 0644 lib/crosstex/style/*.py ${PACKAGE}/lib/crosstex/style - install -m 0644 tests/*.tex tests/*.xtx ${PACKAGE}/tests - install debian/* ${PACKAGE}/debian - sed -e "1i %define name crosstex" \ - -e "1i %define version ${VERSION}" \ - -e "1i %define release ${RELEASE}" \ - -e "1i %define prefix ${PREFIX}" \ - -e "1i %define bindir ${BINDIR}" \ - -e "1i %define libdir ${LIBDIR}" \ - -e "1i %define mandir ${MANDIR}" \ - -e "1i %define ply ${PLY}" \ - crosstex.spec >${PACKAGE}/crosstex.spec - tar czf ${PACKAGE}.tar.gz ${PACKAGE} - -.PHONY: dist -dist: ${PACKAGE}.tar.gz - -.PHONY: rpm -rpm: dist - mkdir -p ${PACKAGE}-rpm/{BUILD,RPMS,SOURCES,SPECS,SRPMS} - rpmbuild -ta --define "_topdir `pwd`/${PACKAGE}-rpm" ${PACKAGE}.tar.gz - find ${PACKAGE}-rpm -name \*.rpm -exec mv {} . \; - rm -r ${PACKAGE}-rpm - -.PHONY: deb -deb: dist - cp ${PACKAGE}.tar.gz crosstex_${VERSION}.orig.tar.gz - (cd ${PACKAGE} && dpkg-buildpackage -rfakeroot) - -clean: - rm -rf *~ *.pyc *.aux *.bbl *.dvi *.log *.tar.gz *.rpm *.html \ - *.out *.toc *.pdf *.haux *.htoc *-rpm *.bak ${PACKAGE} \ - crosstex_${VERSION}* `find lib -name \*.pyc` \ - {data,tests}/.*.cache diff --git a/bin/crosstex b/bin/crosstex index 4781ef1..8714632 100755 --- a/bin/crosstex +++ b/bin/crosstex @@ -1,189 +1,4 @@ #! /usr/bin/env python - -# XXX find paths to libraries and system databases using kpsewhich -version = 'Testing' -xtxlib = 'lib' -plylib = '/usr/local/lib/crosstex' - import sys -sys.path.append(xtxlib) -if plylib not in sys.path: - sys.path.append(plylib) - -import copy -import math -import pipes -import re -import optparse -import string -import tempfile -import imp -import os.path - -from crosstex.options import OptionParser, warning, error -from crosstex.resolve import Database -from crosstex.parse import Parser -import crosstex.style.common -import crosstex.objects - - -optparser = OptionParser(version, [xtxlib]) -namearg = '--' + os.path.basename(sys.argv[0]) -if optparser.has_option(namearg): - sys.argv.insert(1, namearg) - -opts, files = optparser.parse_args() -if not files: - optparser.print_help() - sys.exit(0) - -dbparser = Parser(opts, optparser) -paths = dbparser.files(files) -path = '.'.join([os.path.splitext(paths[-1])[0], opts.convert]) -dbparser.parse('standard') -try: - if opts.convert == 'html': - t = pipes.Template() - t.append('hevea -O', '--') - out = t.open(path, 'w') - else: - out = open(path, 'w') -except IOError, details: - error(opts, 'Unable to open output file "%s": %s.' % (path, details)) - sys.exit(opts.errors) - -if opts.style is None: - if opts.convert == 'bbl': - opts.style = 'plain' - else: - opts.style = opts.convert -try: - # XXX Use kpathsea - mod = None - try: - mod = imp.find_module(opts.style, [os.path.basename(path)]) - stylemodule = imp.load_module(opts.style, *mod) - finally: - if mod and mod[0]: - mod[0].close() -except ImportError, details: - try: - stylemodule = __import__('crosstex.style.' + opts.style) - stylemodule = getattr(stylemodule.style, opts.style) - except ImportError, details: - error(opts, 'Can not load %s style: %s\n' % (opts.style, details)) - sys.exit(opts.errors) -if opts.style in ['unsrt', 'ieeetr']: - dbparser.addopts(['--no-sort']) -if opts.style == 'html': - dbparser.addopts(['--blank-labels', '--break-lines']) - if opts.link == []: - dbparser.addopts(['-l', 'Abstract', '-l', 'URL', '-l', 'PS', '-l', - 'PDF', '-l', 'HTML', '-l', 'DVI', '-l', 'TEX', '-l', - 'BIB', '-l', 'FTP', '-l', 'HTTP', '-l', 'RTF']) -for kind in opts.capitalize: - getattr(crosstex.objects, kind)._addfilter(crosstex.style.common.uppercasefilter, 'value') -for field in opts.no_field: - crosstex.objects.publication._addfilter(crosstex.style.common.killfilter, field) -if opts.add_proc: - crosstex.objects.publication._addfilter(crosstex.style.common.procfilter, 'fullpublication', 'booktitle') -if opts.add_proceedings: - crosstex.objects.publication._addfilter(crosstex.style.common.proceedingsfilter, 'fullpublication', 'booktitle') -if opts.add_in: - crosstex.objects.publication._addfilter(crosstex.style.common.infilter, 'fullpublication', 'booktitle') - crosstex.objects.publication._addfilter(crosstex.style.common.infilter, 'fullpublication', 'journal') -if opts.titlecase == 'lower': - crosstex.objects.publication._addfilter(crosstex.style.common.lowertitlecasefilter, 'title') -elif opts.titlecase == 'upper': - crosstex.objects.publication._addfilter(crosstex.style.common.uppercasefilter, 'title') -elif opts.titlecase == 'default': - crosstex.objects.publication._addfilter(crosstex.style.common.titlecasefilter, 'title') -if dbparser.titlephrases and opts.titlecase != 'upper': - crosstex.objects.publication._addfilter(crosstex.style.common.maketitlephrasefilter(dbparser.titlephrases), 'title') -if dbparser.titlesmalls and opts.titlecase != 'upper': - crosstex.objects.publication._addfilter(crosstex.style.common.makelowerphrasefilter(dbparser.titlesmalls), 'title') -if opts.last_first: - crosstex.objects.publication._addlistfilter(crosstex.style.common.lastfirstlistfilter, 'author') - crosstex.objects.publication._addlistfilter(crosstex.style.common.lastfirstlistfilter, 'editor') -if opts.link: - crosstex.objects.publication._addproducer(crosstex.style.common.makelinksproducer(opts.link), 'links') -if opts.abstract or opts.keywords: - crosstex.objects.publication._addproducer(crosstex.style.common.extrasproducer, 'extras') - if not opts.abstract: - crosstex.objects.publication._addfilter(crosstex.style.common.killfilter, 'extras', 'abstract') - if not opts.keywords: - crosstex.objects.publication._addfilter(crosstex.style.common.killfilter, 'extras', 'keywords') -if opts.title_head: - crosstex.objects.publication._addfilter(crosstex.style.common.boldfilter, 'fulltitle') -if opts.cite_by == 'number': - crosstex.objects.publication._addproducer(crosstex.style.common.emptyproducer, 'label') -elif opts.cite_by == 'initials': - crosstex.objects.publication._addproducer(crosstex.style.common.makegetterproducer('initialslabel'), 'label') -elif opts.cite_by == 'fullname': - crosstex.objects.publication._addproducer(crosstex.style.common.makegetterproducer('fullnamelabel'), 'label') -if opts.title_head: - crosstex.objects.publication._addproducer(crosstex.style.common.makejoinproducer(".", "\n\\newblock ", ".", "", 'fulltitle', 'fullauthorpublicationextras'), 'value') -for kind in opts.short: - getattr(crosstex.objects, kind)._addproducer(crosstex.style.common.makegetterproducer('shortname'), 'value') - if kind in ['author', 'editor']: - crosstex.objects.publication._addlistfilter(crosstex.style.common.shortnameslistfilter, kind) - -db = Database(dbparser) - -for kind in opts.dump: - if kind in ['file', 'titlephrase', 'titlesmall']: - continue - entries = db.dump(kind) - if entries: - sys.stderr.write('\n%s:\n' % kind.upper()) - entries.sort() - for entry in entries: - sys.stderr.write('%s\n' % entry) - else: - sys.stderr.write('No %s objects defined.\n' % kind) -if 'titlephrase' in opts.dump and db.titlephrases: - sys.stderr.write('\nTITLE PHRASES:\n') - for phrase in db.titlephrases: - sys.stderr.write('%s\n' % phrase) -if 'titlesmall' in opts.dump: - sys.stderr.write('\nTITLE SMALL WORDS:\n') - for small in db.titlesmalls: - sys.stderr.write('%s\n' % small) - -try: - if opts.convert == 'html': - out.write("\\documentclass{report}\n") - out.write("\\usepackage{hyperref}\n") - out.write("\\newstyle{.dt-list}{margin-top:1em}\n") - if opts.popups: - out.write("\\newstyle{.dd-list}{position:relative}\n") - out.write("\\newstyle{.dd-list .quotation}" - "{visibility:hidden;position:absolute;top:100\%;" - "left:4em;margin-top:-1em;padding:1em;" - "border:1px solid \#aaaaaa;background:\#ffffff;" - "z-index:1}\n") - out.write("\\newstyle{.dd-list .quotation *}" - "{margin-left:0;padding-left:0}\n") - out.write("\\newstyle{.dd-list:hover .quotation}" - "{visibility:visible}\n") - out.write("\\begin{document}\n") - if opts.convert == 'html' or opts.convert == 'bbl': - for i in db.preambles: - out.write(i + '\n') - if opts.blank_labels: - out.write("\\makeatletter\\def\\@biblabel#1{}\\makeatother\n") - if opts.break_lines: - out.write("\\renewcommand{\\newblock}{\\\\}") - out.write("\\newcommand{\\etalchar}[1]{$^{#1}$}\n") - if opts.convert == 'bib' or opts.convert == 'xtx': - for i in db.preambles: - out.write('@PREAMBLE { "%s" }\n\n' % i) - db.write(out) - if opts.convert == 'html': - out.write("\\end{document}\n") - out.close() -except IOError, details: - print 'crosstex: %s' % details - opts.errors += 1 - -sys.exit(opts.errors) +import crosstex.cmd +crosstex.cmd.main(sys.argv[1:]) diff --git a/crosstex/__init__.py b/crosstex/__init__.py index e69de29..a64762f 100644 --- a/crosstex/__init__.py +++ b/crosstex/__init__.py @@ -0,0 +1,323 @@ +'''Parse files containing CrossTeX databases and citations. + +Care is taken to ensure that even in the face of errors, CrossTeX does as much +as possible. Additionally, errors are never thrown except when necessary for +the bibliography the user wants. Entries that are not cited or use in +inheritance will never be resolved or touched beyond the initial parsing, which +is designed to be fast and error-free. This is for efficiency as much as peace +of mind. To check every entry in a database, simply cite every entry, and then +they will all have to be resolved and checked. +''' + +import logging + +# Setup logging before importing any crosstex things +logging.basicConfig(format='%(message)s') + +import copy +import re + +import crosstex.objects +import crosstex.parse + +import inspect # XXX +logger = logging.getLogger('crosstex') + +class UNIQUE(object): pass + +_author = re.compile('\s+and\s+') + +class Constraint(object): + 'A convenience representation of a constrained citation.' + + def __init__(self, key): + self._fields = {} + parts = key[1:].lower().split(':') + for i in range(len(parts)): + constraint = parts[i].split('=', 1) + if len(constraint) == 1: + try: + values = int(constraint[0]) + except ValueError: + if i == 0: + field = 'author' + else: + field = 'title' + values = constraint[0].split('-') + else: + field = 'year' + values = [str(values)] + else: + field = constraint[0] + values = constraint[1].split('-') + if not field: + logger.error(self.options, 'Empty field in constraint "%s".' % key) + if not values: + logger.error(self.options, 'Empty values in constraint "%s".' % key) + if field and values: + self._fields[field] = values + + def empty(self): + return not len(self._fields) + + def match(self, entry): + entry = entry[0] + for field, values in self._fields.iteritems(): + if not hasattr(entry, field): + return False + tocheck = getattr(entry, field) + strings = set([]) + if isinstance(tocheck, crosstex.parse.Value): + strings.add(str(tocheck.value).lower()) + elif isinstance(tocheck, crosstex.objects.string): + strings.add(str(tocheck.name.value).lower()) + strings.add(str(tocheck.shortname.value).lower()) + strings.add(str(tocheck.longname.value).lower()) + elif field == 'author' and isinstance(tocheck, list): + for a in tocheck: + strings.add(str(a.value).lower()) + strings = tuple(strings) + for value in values: + v = value.lower() + found = False + for s in strings: + if v in s: + found = True + break + if not found: + return False + return True + +class Database(object): + + def __init__(self): + self._path = ['.'] + self._parser = crosstex.parse.Parser(self._path) + self._cache = {} + + def append_path(self, path): + self._path.append(path) + self._parser.set_path(self._path) + + def parse_file(self, path): + self._parser.parse(path) + + def aux_citations(self): + return copy.copy(self._parser.citations) + + def all_citations(self): + return copy.copy(self._parser.entries.keys()) + + def titlephrases(self): + return copy.copy(self._parser.titlephrases) + + def titlesmalls(self): + return copy.copy(self._parser.titlesmalls) + + def lookup(self, key): + if key.startswith('!'): + return self._semantic_lookup(key) + return self._lookup(key)[0] + + def _semantic_lookup(self, key, context=None): + '''Resolve an entry matching the constrained citation.''' + const = Constraint(key) + if const.empty(): + logger.error('Empty constraint "%s".' % key) + return None + matches = [] + for k in self._parser.entries.keys(): + obj = self._lookup(k) + if obj and const.match(obj): + matches.append(obj) + if not matches: + return None + if len(matches) > 1: + logger.warning('Constraint "%s" matches multiple objects' % key) + return matches[0][0] + + def _lookup(self, key, context=None): + '''Resolve an entry matching the given key or constrained citation. + + Evaluate conditional fields, inheritance, @extend entries, etc. until + the entry is stable and return the result. + ''' + # Check for loops + context = list(context or []) + if key in context: + context.append(key) + logger.error('There is a reference cycle: %s' % ', '.join(context)) + return (None, None) + context.append(key) + # This makes things about 30% faster + if key in self._cache: + return self._cache[key] + # Lookup all raw Entry objects + keys, base, extensions = self._select(key) + if base is None: + return (None, None) + # Get the class from crosstex.objects that corresponds to this object + if not hasattr(crosstex.objects, base.kind): + logger.error('%s:%d: There is no such thing as a @%s.' % (base.file, base.line, base.kind)) + return (None, None) + kind = getattr(crosstex.objects, base.kind) + if not isinstance(kind, type) or \ + not issubclass(kind, crosstex.objects.Object): + logger.error('%s:%d: There is no such thing as a @%s.' % (base.file, base.line, base.kind)) + return (None, None) + fields = {} + conditionals = [] + # This loop iterates through the base and extensions, applying fields + # from each. + for entry in [base] + extensions: + dupes = set([]) + for name, value in entry.defaults: + if name in kind.allowed and name not in fields: + fields[name] = value + for f in entry.fields: + if not isinstance(f, crosstex.parse.Field): + continue + if f.name in dupes: + logger.warning('%s:%d: %s field is duplicated.' % + (f.value.file, f.value.line, f.name)) + elif f.name not in kind.allowed: + logger.warning('%s:%d: No such field %s in %s.' % + (f.value.file, f.value.line, f.name, base.kind)) + else: + fields[f.name] = f.value + dupes.add(f.name) + for c in entry.fields: + if not isinstance(c, crosstex.parse.Conditional): + continue + conditionals.append(c) + assert all([isinstance(c, crosstex.parse.Conditional) for c in conditionals]) + # This loop resolves conditionals or references until the object reaches + # a fixed point + anotherpass = True + applied_conditionals = set([]) + while anotherpass: + anotherpass = False + # This loop pulls references from other objects + for name, value in fields.iteritems(): + if not isinstance(value, crosstex.parse.Value): + continue + if value.kind != 'key': + continue + obj, conds = self._lookup(value.value, context) + if obj is not None: + assert conds is not None + fields[name] = obj + conditionals = conds + conditionals + anotherpass = True + break + # We want to make only one change at a time + if anotherpass: + continue + # This loop applies conditionals + for c in reversed(conditionals): + if c in applied_conditionals: + continue + # Skip if not all of the fields match + if not all([fields[f.name].kind == f.value.kind and + fields[f.name].value == f.value.value + for f in c.if_fields if f.name in fields] + + [False for f in c.if_fields if f.name not in fields]): + continue + dupes = set([]) + for f in c.then_fields: + if f.name in dupes: + logger.warning('%s:%d: %s field is duplicated.' % + (f.file, f.line, field.name, base.kind)) + elif f.name in kind.allowed and f.name not in fields: + # no warning here because we just percolate these + # things up willy-nilly + fields[f.name] = f.value + dupes.add(f.name) + elif f.name in kind.allowed: + logger.warning('%s:%d: %s field not applied from conditional at %s:%d' % + (base.file, base.line, f.name, c.file, c.line)) + applied_conditionals.add(c) + # This loop expands author/editor fields + for name, value in fields.iteritems(): + if name not in ('author', 'editor'): + continue + if not isinstance(value, crosstex.parse.Value): + continue + names = [] + for n in _author.split(value.value): + if n in self._parser.entries: + obj, conds = self._lookup(n, context) + else: + obj, conds = None, None + if obj is not None: + assert conds is not None + names.append(obj) + conditionals = conds + conditionals + else: + names.append(crosstex.parse.Value(file=value.file, line=value.line, kind='string', value=n)) + fields[name] = names + anotherpass = True + break + # Do a pass over alternate fields to copy them + for name, alternates in kind.alternates.iteritems(): + if name not in fields: + for f in alternates: + if f in fields: + fields[name] = fields[f] + break + # Ensure required fields are all provided + for name in kind.required: + if name not in fields: + logger.error('%s:%d: Missing required field %s in %s.' % + (base.file, base.line, name, base.kind)) + # Create the object + k = kind(**fields) + # Memoize + for key in keys: + self._cache[key] = (k, conditionals) + return k, conditionals + + def _select(self, key): + '''Select Entry objects tagged with "key". + + Aliases will be transitively followed to select all Entry objects. + + The return value will be a tuple of (keys, base, extensions). "keys" + will be a set consisting of key and all its aliases. "base" will be the + Entry object that the key maps to. "extensions" will be a list of + objects that extend "base". + ''' + keys = set([]) + todo = set([key]) + seen = set([]) + base = None + extensions = [] + while todo: + key = todo.pop() + keys.add(key) + for entry in self._parser.entries.get(key, []): + if entry in seen: + continue; + seen.add(entry) + if entry.kind == 'extend': + extensions.append(entry) + elif base is None: + base = entry + else: + logger.error('%s:%d: Alias %s is also defined at %s:%d.' % + (base.file, base.line, key, entry.file, entry.line)) + continue + for k in entry.keys: + if k not in keys: + todo.add(k) + if base is None: + if extensions: + logger.error('%s is extended but never defined.' % key) + else: + logger.error('%s is never defined.' % key) + # XXX provide a guaranteed order for the extensions. + # In an ideal world we'd traverse includes in a DFS manner according to + # include order, thus guaranteeing that extensions will be resolved in a + # consistent fashion. Right now, conflicting extensions will be + # resolved in a predictable manner + return (keys, base, extensions) diff --git a/crosstex/cmd.py b/crosstex/cmd.py new file mode 100644 index 0000000..6d278cc --- /dev/null +++ b/crosstex/cmd.py @@ -0,0 +1,206 @@ +import argparse +import importlib +import logging +import os +import os.path +import sys + +import crosstex +import crosstex.style + + +logger = logging.getLogger('crosstex') + + +parser = argparse.ArgumentParser(prog='crosstex', + description='A modern, object-oriented bibliographic tool.') +#parser.add_argument('--quiet', +# help='Do not sanity check the input (XXX: ignored.') +#parser.add_argument('--strict', +# help='Apply stricter checks and check all entries (XXX:ignored.') +#parser.add_argument('--dump', metavar='TYPE', +# help='After parsing the bibliography, dump a list of all ' +# 'objects of the type specified, or, with "file", print ' +# 'a list of files processed. XXX: ignored') +#parser.add_argument('--cite-by', metavar='CITE_BY', default='style', +# help='With "number", use numeric labels such as [1]. With ' +# '"initials", use labels based on author last-names such ' +# 'as [SBRD04b]. With "fullname", use labels based on ' +# 'author names such as [Sirer et al. 2004]. With ' +# '"style", use the style default. XXX:ignored') +#parser.add_argument('-s', '--sort', metavar='FIELD', action='append', +# help='Sort by specified field. Multiple sort orders are ' +# 'applied in the order specified, e.g. "-s year -s ' +# 'author" will cause elements to be grouped primarily by ' +# 'author and sub-grouped by year.' +# ' XXX: this is currently ignored') +#parser.add_argument('-S', '--reverse-sort', metavar='FIELD', action='append', +# help='Exactly as --sort, but sort by descending field values ' +# 'rather than ascending.' +# ' XXX: this is currently ignored') +#parser.add_argument('--no-sort', help='XXX: ignored') +#parser.add_argument('--heading', metavar='FIELD', +# help='Divide entries and create headings in bibliography by ' +# 'the value of the given field. XXX: ignored') +#parser.add_argument('--reverse-heading', metavar='FIELD', +# help='Exactly as --heading, but sort by descending field ' +# 'values rather than ascending. XXX: ignored') +#parser.add_argument('--capitalize', metavar='TYPE', action='append', +# help='Specify any string-like object, i.e. one with name and ' +# 'shortname fields. Strings of the specified types will ' +# 'appear in ALL CAPS. XXX: ignored') +#parser.add_argument('--no-field', metavar='TYPE', action='append', +# help='Specify a field name, and in any objects where that ' +# 'field is optional it will be unassigned no matter what ' +# 'appears in the database. For example, to turn off ' +# 'page numbers, use "--no-field pages". XXX: ignored') +#parser.add_argument('-l', '--link', metavar='FIELD', action='append', +# help='Add to the list of fields used to generate links. ' +# 'LaTeX documents should make use of links by including ' +# 'the hyperref package. When converting to HTML, this ' +# 'defaults to [Abstract, URL, PS, PDF, HTML, DVI, TEX, ' +# 'BIB, FTP, HTTP, and RTF]. XXX: ignored') +#parser.add_argument('--no-link', help='XXX: ignored') +#parser.add_argument('--abstract', +# help='In the bibliography, include paper abstracts if available. XXX: ignored') +#parser.add_argument('--no-abstract') +#parser.add_argument('--keywords', +# help='In the bibliography, include paper keywords if available. XXX: ignored') +#parser.add_argument('--no-keywords') +#parser.add_argument('--popups', +# help='If abstracts or keywords are to appear for an entry' +# 'when generating HTML, instead hide these extra blocks' +# 'and reveal them as popups when the mouse hovers over' +# 'the entry. XXX: ignored') +#parser.add_argument('--no-popups') +#parser.add_argument('--title-head', +# help='In the bibliography, put the title bold and first. XXX:ignored') +#parser.add_argument('--no-title-head') +#parser.add_argument('--blank-labels', +# help='In the bibliography, leave out item labels. XXX:ignored') +#parser.add_argument('--no-blank-labels') +#parser.add_argument('--break-lines', +# help='In the bibliography, put author, title, and ' +# 'publication information on separate lines. XXX:ignored') +#parser.add_argument('--no-break-lines') +#parser.add_argument('--last-first', +# help='The first name in each author list will appear "Last, ' +# 'First" instead of "First Last" (the latter is the ' +# 'default). XXX:ignored') +#parser.add_argument('--no-last-first') +parser.add_argument('--version', version='CrossTeX 0.7.0', action='version') +parser.add_argument('-d', '--dir', metavar='DIR', action='append', dest='dirs', + help='Add a directory in which to find data files, searched ' + 'from last specified to first.') +parser.add_argument('--cite', metavar='CITE', action='append', + help='Cite a key exactly as with the \cite LaTeX command.') +parser.add_argument('--style', metavar='STYLE', default='plain', + help='Use a standard style such as plain, unsrt, abbrv, ' + 'full, or alpha. Options set by the style may be ' + 'overidden by further command-line options.') +parser.add_argument('--short', metavar='TYPE', action='append', + help='Specify any string-like object, i.e. one with name and ' + 'shortname fields. Whenever possible,the short name ' + 'will be used, e.g. two-letter state codes for ' + '"state", conference acronyms such as NSDI for ' + '"conference", or initials such as E. G. Sirer for ' + '"author".') +parser.add_argument('--titlecase', metavar='TITLECASE', default='default', + choices=('default', 'lower', 'upper', 'title'), + help='In the bibliography, force titles into lower-, upper-, ' + 'or title-case. Default: leave the titles unchanged.') +parser.add_argument('-f', '--format', metavar='FORMAT', dest='fmt', default='bbl', + help='Select a format for the output. Examples include ' + '"bbl", "html", "bib", or "xtx". "bib" and "xtx" are ' + 'always available and not affected by "--style". ' + 'Other formats are dependent upon the choice of style.') +parser.add_argument('-o', '--output', metavar='FILE', + help='Write the bibliography to the specified output file.') +parser.add_argument('--add-in', action='store_const', const=True, default=False, + help='Add "In" for articles.') +parser.add_argument('--add-proc', dest='add_proc', + action='store_const', const='proc', default=None, + help='Add "Proc. of" to conference and workshop publications.') +parser.add_argument('--add-proceedings', dest='add_proc', + action='store_const', const='proceedings', + help='Add "Proceedings of the" to conference and workshop publications.') +parser.add_argument('files', metavar='FILES', nargs='+', + help='A list of xtx, aux, or bib files to process.') + + +def main(argv): + args = parser.parse_args() + assert args.dirs + db = crosstex.Database() + for path in args.dirs: + db.append_path(path) + db.append_path(os.path.join(os.path.join(os.path.expanduser('~'), '.crosstex'))) + db.append_path('/XXX') # XXX system crosstex + for f in reversed(args.files): + db.parse_file(f) + flags = set([]) + flags.add('titlecase-' + args.titlecase) + if args.add_in: + flags.add('add-in') + if args.add_proc == 'proc': + flags.add('add-proc') + if args.add_proc == 'proceedings': + flags.add('add-proceedings') + for s in args.short or []: + flags.add('short-' + s) + # Find the right format and style + # 'bib' and 'xtx' are hard-coded and not affected by the style + if args.fmt == 'bib': + logger.error('CrossTeX currently doesn\'t write bib files.') # XXX + return 1 + if args.fmt == 'xtx': + logger.error('CrossTeX currently doesn\'t write xtx files.') # XXX + return 1 + # Otherwise, we need to look up the style object, and see if it can support + # the given format + try: + stylemod = importlib.import_module('crosstex.style.' + args.style) + if not hasattr(stylemod, 'Style'): + logger.error('Style not found.') + return 1 + styleclass = getattr(stylemod, 'Style') + except ImportError as e: + logger.error('Style not found.') + return 1 + style = styleclass(flags, db.titlephrases(), db.titlesmalls()) + # Check if the format and style work well together + if args.fmt not in styleclass.formats(): + logger.error('Style does not support the format.') + return 1 + # We'll use this check later + is_aux = os.path.splitext(args.files[-1])[1] == '.aux' + # Get a list of things to cite + cite = [] + if args.cite: + cite = args.cite + elif is_aux: + cite = db.aux_citations() + else: + cite = db.all_citations() + resolved = [(c, db.lookup(c)) for c in cite] + for c in [c for c, o in resolved if not o or not o.citeable]: + logger.warning('Cannot find object for citation %r' % c) + citeable = [(c, o) for c, o in resolved if o and o.citeable] + citeable.sort(key=style.sort_key) + try: + rendered = style.render(citeable) + except crosstex.style.UnsupportedCitation as e: + logger.error('Style does not support citations for %s' % e.citetype) + return 1 + if args.output: + with open(args.output, 'w') as fout: + fout.write(rendered) + fout.flush() + elif is_aux: + with open(os.path.splitext(args.files[-1])[0] + '.bbl', 'w') as fout: + fout.write(rendered) + fout.flush() + else: + sys.stdout.write(rendered) + sys.stdout.flush() + return 0 diff --git a/crosstex/objects.py b/crosstex/objects.py index 2faab60..4f5623e 100644 --- a/crosstex/objects.py +++ b/crosstex/objects.py @@ -1,438 +1,264 @@ -''' -Information about the objects that can occur in databases. - -The class hierarchy here describes how formatting works, which fields -are required, optional, and can derive values from other fields, etc. - -REQUIRED and OPTIONAL are constants describing requirement levels for -fields in objects. -''' - -import re -from copy import copy - - -REQUIRED = object() -OPTIONAL = object() - - -def cmpclasses(a, b): - 'A comparison function sorting subclasses before parents.' - - le = issubclass(a, b) - ge = issubclass(b, a) - if le and not ge: - return -1 - if ge and not le: - return 1 - return cmp(a.__name__, b.__name__) - - -_producers = {} -_listfilters = {} -_listformatters = {} -_filters = {} - - -class Formatter(object): - - _citeable = False - - def __init__(self, *kargs, **kwargs): - self._value = None - super(Formatter, self).__init__(*kargs, **kwargs) - - def _chain(cls, filter, chain, context): - if not context: - raise ValueError, 'Empty context' - l = chain.setdefault(context, {}).setdefault(cls, []) - v = filter - if not hasattr(v, '__call__'): - v = lambda obj, value, context: filter - if chain is _producers: - l.insert(0, v) - else: - l.append(v) - _chain = classmethod(_chain) - - def _addproducer(cls, filter, *context): - cls._chain(filter, _producers, context) - _addproducer = classmethod(_addproducer) - - def _addlistfilter(cls, filter, *context): - cls._chain(filter, _listfilters, context) - _addlistfilter = classmethod(_addlistfilter) - - def _addlistformatter(cls, filter, *context): - cls._chain(filter, _listformatters, context) - _addlistformatter = classmethod(_addlistformatter) - - def _addfilter(cls, filter, *context): - cls._chain(filter, _filters, context) - _addfilter = classmethod(_addfilter) - - def _filter(self, chain, value, stop, context): - if stop(value): - return value - for i in range(len(context)): - try: - filters = chain[context[i:]] - except KeyError: - continue - kinds = [ t for t in filters if isinstance(self, t) ] - kinds.sort(cmpclasses) - for kind in kinds: - for filter in filters[kind]: - newvalue = filter(self, value, context) - if newvalue is not None: - value = newvalue - if stop(value): - return value - return value - - def _format(self, *context): - listtest = lambda x: x is None or not hasattr(x, '__iter__') - value = self._filter(_producers, None, lambda x: x is not None, context) - value = self._filter(_listfilters, value, listtest, context) - value = self._filter(_listformatters, value, listtest, context) - if value is not None: - value = str(value) - value = self._filter(_filters, value, lambda x: x is None, context) - if value is not None: - value = str(value) - return value - - def __str__(self): - if self._value is None: - self._value = self._format('value') - if self._value is None: - self._value = '' - return self._value - - -class ObjectList(Formatter, list): - def __init__(self, *kargs, **kwargs): - super(ObjectList, self).__init__(*kargs, **kwargs) - - -class Object(Formatter): - def __init__(self, keys, fields, conditions, file, line, *kargs, **kwargs): - super(Object, self).__init__(*kargs, **kwargs) - self.keys = keys - self.file = file - self.line = line - self.kind = type(self).__name__ - self.kind = self.kind[self.kind.rfind('.')+1:] - self.conditions = conditions - self.fields = fields - self.citation = None - - -class Concatenation(list): - def __str__(self): - return ''.join([str(x) for x in self]) - - -# -# The actual objects. -# +import collections +import copy + +import crosstex.parse + +class CiteableTrue(object): + def __get__(self, obj, objtype): + return True + +class CiteableFalse(object): + def __get__(self, obj, objtype): + return False + +class Field(object): + def __init__(self, required=False, alternates=None, types=None, iterable=False): + self.required = required + self.alternates = alternates or [] + self.types = tuple(types or []) + self.types += (str, crosstex.parse.Value) + self.iterable = iterable + self.name = None + def __get__(self, obj, objtype): + if hasattr(obj, '_' + self.name): + return getattr(obj, '_' + self.name) + return None + def __set__(self, obj, value): + if value is not None and \ + value.__class__ not in self.types and \ + not (self.iterable and isinstance(value, collections.Iterable) and + all([isinstance(v, self.types) for v in value])): + raise TypeError('Field %s does not allow type %s' % + (self.name, str(type(value)))) + setattr(obj, '_' + self.name, value) + +class ObjectMeta(type): + def __new__(cls, name, bases, dct): + allowed = set([]) + required = set([]) + alternates = {} + for attr, value in dct.iteritems(): + if attr == 'citeable': + assert value.__class__ in (CiteableTrue, CiteableFalse) + elif not attr.startswith('_') and not callable(value): + assert attr not in ('kind', 'required', 'allowed', 'alternates') + assert isinstance(value, Field) + allowed.add(attr) + if value.required: + required.add(attr) + if isinstance(value.alternates, str): + alternates[attr] = value.alternates + elif isinstance(value.alternates, collections.Iterable): + assert all([isinstance(a, str) for a in value.alternates]) + alternates[attr] = [a for a in value.alternates] + else: + assert False + value.name = attr + optional = allowed - required + assert len(bases) <= 1 + for base in bases: + if hasattr(base, 'allowed'): + allowed |= base.allowed + if hasattr(base, 'required'): + required |= base.required - optional + if hasattr(base, 'alternates'): + newalternates = copy.copy(base.alternates) + newalternates.update(alternates) + alternates = newalternates + del newalternates + dct['kind'] = name + dct['allowed'] = allowed + dct['required'] = required + dct['alternates'] = alternates + return super(ObjectMeta, cls).__new__(cls, name, bases, dct) + +class Object(object): + __metaclass__ = ObjectMeta + citeable = CiteableFalse() + + def __init__(self, **kwargs): + for key, word in kwargs.iteritems(): + assert not key.startswith('_') and hasattr(self, key) + setattr(self, key, word) + + def isset_field(self, name): + return getattr(self, name, None) is not None + + def set_field(self, name, value): + setattr(self, name, value) + + def iteritems(self): + for f in self.allowed: + v = getattr(self, f, None) + yield (f, v) + +############################################################################### class string(Object): - name = ['longname', 'shortname'] - shortname = [REQUIRED, 'name', 'longname'] - longname = [REQUIRED, 'name', 'shortname'] - -class author(string): - address = OPTIONAL - affiliation = OPTIONAL - email = OPTIONAL - institution = OPTIONAL - organization = OPTIONAL - phone = OPTIONAL - school = OPTIONAL - url = OPTIONAL - -class state(string): - country = OPTIONAL + name = Field(alternates=('longname', 'shortname')) + shortname = Field(required=True, alternates=('name', 'longname')) + longname = Field(required=True, alternates=('name', 'shortname')) class country(string): pass +class state(string): + country = Field(types=(country,)) + class location(string): - name = [REQUIRED, 'longname', 'shortname', 'city', 'state', 'country'] - shortname = ['name', 'longname'] - longname = ['name', 'shortname'] - city = OPTIONAL - state = OPTIONAL - country = OPTIONAL + name = Field(required=True, alternates=('longname', 'shortname', 'city', 'state', 'country')) + city = Field() + state = Field(types=(state,)) + country = Field(types=(country,)) class month(string): - monthno = REQUIRED - -class journal(string): - pass + monthno = Field() class institution(string): - address = OPTIONAL + address = Field(types=(location, country, state)) class school(institution): pass -class publication(Object): - _citeable = True - - abstract = OPTIONAL - address = OPTIONAL - affiliation = OPTIONAL - annote = OPTIONAL - author = OPTIONAL - bib = OPTIONAL - bibsource = OPTIONAL - booktitle = OPTIONAL - category = OPTIONAL - subcategory = OPTIONAL - chapter = OPTIONAL - contents = OPTIONAL - copyright = OPTIONAL - crossref = OPTIONAL - doi = OPTIONAL - dvi = OPTIONAL - edition = OPTIONAL - editor = OPTIONAL - ee = OPTIONAL - ftp = OPTIONAL - howpublished = OPTIONAL - html = OPTIONAL - http = OPTIONAL - institution = OPTIONAL - isbn = OPTIONAL - issn = OPTIONAL - journal = 'newspaper' - newspaper = 'journal' - key = OPTIONAL - keywords = OPTIONAL - language = OPTIONAL - lccn = OPTIONAL - location = OPTIONAL - month = OPTIONAL - monthno = OPTIONAL - mrnumber = OPTIONAL - note = OPTIONAL - number = OPTIONAL - organization = OPTIONAL - pages = OPTIONAL - pdf = OPTIONAL - price = OPTIONAL - ps = OPTIONAL - publisher = OPTIONAL - rtf = OPTIONAL - school = OPTIONAL - series = OPTIONAL - size = OPTIONAL - title = OPTIONAL - type = OPTIONAL - url = OPTIONAL - volume = OPTIONAL - year = OPTIONAL - -class misc(publication): - pass - -class article(publication): - author = REQUIRED - title = REQUIRED - journal = [REQUIRED, 'newspaper'] - newspaper = 'journal' - year = REQUIRED - -class book(publication): - author = REQUIRED - editor = OPTIONAL - title = REQUIRED - publisher = REQUIRED - year = REQUIRED - -class booklet(publication): - title = REQUIRED - -class inbook(publication): - author = REQUIRED - editor = REQUIRED - title = REQUIRED - chapter = REQUIRED - pages = REQUIRED - publisher = REQUIRED - year = REQUIRED - -class incollection(publication): - author = REQUIRED - title = REQUIRED - booktitle = REQUIRED - publisher = REQUIRED - year = REQUIRED - -class inproceedings(publication): - author = REQUIRED - title = REQUIRED - booktitle = REQUIRED - year = REQUIRED - -class manual(publication): - title = REQUIRED - -class thesis(publication): - author = REQUIRED - title = REQUIRED - school = REQUIRED - year = REQUIRED - -class mastersthesis(thesis): - pass - -class phdthesis(thesis): - pass +class author(string): + address = Field(types=(location, country, state)) + affiliation = Field() + email = Field() + institution = Field() + organization = Field() + phone = Field() + school = Field() + url = Field() -class proceedings(publication): - title = REQUIRED - year = REQUIRED +############################################################################### -class collection(proceedings): +class journal(string): pass -class patent(publication): - author = REQUIRED - title = REQUIRED - number = REQUIRED - month = REQUIRED - year = REQUIRED - -class techreport(publication): - author = REQUIRED - title = REQUIRED - institution = REQUIRED - year = REQUIRED - -class unpublished(publication): - author = REQUIRED - title = REQUIRED - note = REQUIRED - class conference(string): - abstract = OPTIONAL - address = OPTIONAL - affiliation = OPTIONAL - annote = OPTIONAL - author = OPTIONAL - bib = OPTIONAL - bibsource = OPTIONAL - booktitle = OPTIONAL - category = OPTIONAL - subcategory = OPTIONAL - chapter = OPTIONAL - contents = OPTIONAL - copyright = OPTIONAL - crossref = OPTIONAL - doi = OPTIONAL - dvi = OPTIONAL - edition = OPTIONAL - editor = OPTIONAL - ee = OPTIONAL - ftp = OPTIONAL - howpublished = OPTIONAL - html = OPTIONAL - http = OPTIONAL - institution = OPTIONAL - isbn = OPTIONAL - issn = OPTIONAL - journal = 'newspaper' - newspaper = 'journal' - key = OPTIONAL - keywords = OPTIONAL - language = OPTIONAL - lccn = OPTIONAL - location = OPTIONAL - month = OPTIONAL - monthno = OPTIONAL - mrnumber = OPTIONAL - note = OPTIONAL - number = OPTIONAL - organization = OPTIONAL - pages = OPTIONAL - pdf = OPTIONAL - price = OPTIONAL - ps = OPTIONAL - publisher = OPTIONAL - rtf = OPTIONAL - school = OPTIONAL - series = OPTIONAL - size = OPTIONAL - title = OPTIONAL - type = OPTIONAL - url = OPTIONAL - volume = OPTIONAL - year = OPTIONAL + pass class conferencetrack(conference): - conference = OPTIONAL + conference = Field(types=(conference,)) class workshop(conferencetrack): - pass - -class rfc(publication): - author = REQUIRED - title = REQUIRED - number = REQUIRED - month = REQUIRED - year = REQUIRED - -class url(publication): - url = REQUIRED - accessmonth = OPTIONAL - accessyear = OPTIONAL - accessday = OPTIONAL - -class newspaperarticle(article): - author = OPTIONAL + conference = Field(types=(conference,)) + +############################################################################### + +def Author(required=False): + return Field(required=True, types=(author,), iterable=True) + +def BookTitle(required=False): + return Field(required=required, types=(conference, conferencetrack, workshop)) + +############################################################################### + +class citeableref(Object): + citeable = CiteableTrue() + +class article(citeableref): + author = Author(required=True) + title = Field(required=True) + journal = Field(required=True, alternates=('newspaper'), types=(journal,)) + year = Field(required=True) + #month = Field(types=(month,)) + volume = Field() + number = Field() + pages = Field() + +class book(citeableref): + author = Author(required=True) + editor = Field() + title = Field(required=True) + publisher = Field(required=True) + address = Field(required=True) + year = Field(required=True) + +class booklet(citeableref): + title = Field(required=True) + +class inbook(citeableref): + author = Author(required=True) + editor = Field() + title = Field(required=True) + chapter = Field(required=True) + pages = Field(required=True) + publisher = Field(required=True) + year = Field(required=True) + +class incollection(citeableref): + author = Author(required=True) + title = Field(required=True) + booktitle = BookTitle(required=True) + publisher = Field(required=True) + year = Field(required=True) + +class inproceedings(citeableref): + author = Author(required=True) + title = Field(required=True) + booktitle = BookTitle(required=True) + pages = Field() + address = Field(types=(location, country, state)) + year = Field() + month = Field(types=(month,)) + +class manual(citeableref): + title = Field(required=True) + +class misc(citeableref): + author = Author() + title = Field() + howpublished = Field() + booktitle = BookTitle() + address = Field(types=(location, country, state)) + year = Field() + +class patent(citeableref): + author = Author(required=True) + title = Field(required=True) + number = Field(required=True) + month = Field(required=True, types=(month,)) + year = Field(required=True) + +class rfc(citeableref): + author = Author(required=True) + title = Field(required=True) + number = Field(required=True) + month = Field(required=True, types=(month,)) + year = Field(required=True) + +class techreport(citeableref): + author = Author(required=True) + title = Field(required=True) + number = Field() + institution = Field(required=True) + address = Field(types=(location, country, state)) + year = Field(required=True) + month = Field(types=(month,)) + +class thesis(citeableref): + author = Author(required=True) + title = Field(required=True) + school = Field(required=True) + number = Field() + year = Field(required=True) -class newspaper(journal): +class mastersthesis(thesis): pass +class phdthesis(thesis): + pass -# -# Initialization -# - -def _fieldproducer(obj, value, context): - try: - return obj.fields.get(context[-1], None) - except AttributeError: - return - except IndexError: - return - -for kind in globals().values(): - if not (isinstance(kind, type) and issubclass(kind, Object)): - continue - - kind._required = {} - kind._allowed = {} - kind._fillin = {} - - for field in dir(kind): - if field.startswith('_'): - continue - kind._allowed[field] = True - value = getattr(kind, field) - - if hasattr(value, '__iter__'): - if REQUIRED in value: - kind._required[field] = True - kind._fillin[field] = [str(f) for f in value \ - if f is not REQUIRED and f is not OPTIONAL] - else: - if value is REQUIRED: - kind._required[field] = True - elif value is not OPTIONAL: - kind._fillin[field] = [str(value)] - - kind._addproducer(_fieldproducer, field) +class unpublished(citeableref): + author = Author(required=True) + title = Field(required=True) + note = Field(required=True) + +class url(citeableref): + author = Author() + title = Field() + url = Field(required=True) + accessmonth = Field(types=(month,)) + accessyear = Field() + accessday = Field() diff --git a/crosstex/options.py b/crosstex/options.py deleted file mode 100755 index 34234c2..0000000 --- a/crosstex/options.py +++ /dev/null @@ -1,273 +0,0 @@ -''' -Handle CrossTeX's many command-line options. - -This is somewhat complicated further by the fact that options can be -set in .aux files on a per-document, on-the-fly basis using \bibstyle. -It is therefore a desirable property to build up options iteratively. -''' - -import sys -import optparse -import crosstex.objects - - -def heading(option, opt, value, parser, reversed=False): - parser.values.heading.append((reversed, value)) - -def sorting(option, opt, value, parser, reversed=False): - if parser.values.sort == None or value == 'none': - parser.values.sort = [] - if value != 'none': - parser.values.sort.append((reversed, value)) - -def message(opts, level, details): - if level >= opts.check: - if level > 0: - opts.errors += 1 - else: - opts.warnings += 1 - sys.stderr.write(str(details) + '\n') - -def warning(opts, details): - message(opts, 0, details) - -def error(opts, details): - message(opts, 1, details) - - -_strings = [ name for name in dir(crosstex.objects) \ - if (isinstance(getattr(crosstex.objects, name), type) and \ - issubclass(getattr(crosstex.objects, name), \ - crosstex.objects.string)) ] - -_dumpable = ['file', 'titlephrase', 'titlesmall'] + _strings - - -class OptionParser(optparse.OptionParser): - - def __init__(self, version='Testing', xtxlib=[]): - optparse.OptionParser.__init__(self, - usage='usage: %prog [options] files', - version='CrossTeX %s' % version, - description='A modern, object-oriented bibliographic tool.') - - self.set_defaults(errors=0) - self.set_defaults(warnings=0) - - self.set_defaults(check=1) - self.add_option('--quiet', - action='store_const', - const=2, - dest='check', - help='Do not sanity check the input.') - self.add_option('--strict', - action='store_const', - const=0, - dest='check', - help='Apply stricter checks and check all entries.') - - self.set_defaults(dump=[]) - self.add_option('--dump', - metavar='TYPE', - action='append', - type='choice', - choices=_dumpable, - help='After parsing the bibliography, dump a list of all objects of the ' - 'type specified, or, with "file", print a list of files processed.') - - dirs = xtxlib - if '.' not in dirs: - dirs += '.' - self.set_defaults(dir=dirs) - self.add_option('-d', '--dir', - type='string', - action='append', - help='Add a directory in which to find data files, searched from last ' - 'specified to first. Default: %default') - - self.set_defaults(cite=[]) - self.add_option('--cite', - type='string', - action='append', - help='Cite a key exactly as with the \\cite LaTeX command.') - - self.set_defaults(cite_by='style') - self.add_option('--cite-by', - type='choice', - choices=['number', 'initials', 'fullname', 'style'], - help='With "number", use numeric labels such as [1]. With "initials", ' - 'use labels based on author last-names such as [SBRD04b]. With ' - '"fullname", use labels based on author names such as ' - '[Sirer et al. 2004]. With "style", use the style default.') - - self.set_defaults(style=None) - self.add_option('--style', - metavar='STYLE', - type='string', - help='Use a standard style such as plain, unsrt, abbrv, full, or alpha. ' - 'Options set by the style may be overidden by further command-line ' - 'options.') - - self.set_defaults(sort=[(False, 'title'), (False, 'monthno'), - (False, 'year'), (False, 'author')]) - self.add_option('-s', '--sort', - metavar='FIELD', - type='string', - action='callback', - callback=sorting, - help='Sort by specified field. Multiple sort orders are applied in the ' - 'order specified, e.g. "-s year -s author" will cause elements to ' - 'be grouped primarily by author and sub-grouped by year.') - self.add_option('-S', '--reverse-sort', - metavar='FIELD', - type='string', - action='callback', - callback=sorting, - callback_args=(True,), - help='Exactly as --sort, but sort by descending field values rather ' - 'than ascending.') - self.add_option('--no-sort', - action='store_const', - const=[], - dest='sort') - - self.set_defaults(heading=[]) - self.add_option('--heading', - metavar='FIELD', - type='string', - action='callback', - callback=heading, - help='Divide entries and create headings in bibliography by the value ' - 'of the given field.') - self.add_option('--reverse-heading', - metavar='FIELD', - type='string', - action='callback', - callback=heading, - callback_args=(True,), - help='Divide entries and create headings in bibliography by the value ' - 'of the given field.') - - self.set_defaults(short=[]) - self.add_option('--short', - metavar='TYPE', - type='choice', - action='append', - choices=_strings, - help='Specify any string-like object, i.e. one with name and shortname ' - 'fields. Whenever possible,the short name will be used,e.g. ' - 'two-letter state codes for "state",conference acronyms such as ' - 'NSDI for "conference",or initials such as E. G. Sirer for ' - '"author".') - - self.set_defaults(capitalize=[]) - self.add_option('--capitalize', - metavar='TYPE', - type='choice', - choices=_strings, - action='append', - help='Specify any string-like object, i.e. one with name and shortname ' - 'fields. Strings of the specified types will appear in ALL CAPS.') - - self.set_defaults(no_field=[]) - self.add_option('--no-field', - metavar='FIELD', - action='append', - help='Specify a field name, and in any objects where that field is ' - 'optional it will be unassigned no matter what appears in the ' - 'database. For example, to turn off page numbers, use ' - '"--no-field pages".') - - self.set_defaults(link=[]) - self.add_option('-l', '--link', - metavar='FIELD', - type='string', - action='append', - help='Add to the list of fields used to generate links. LaTeX documents ' - 'should make use of links by including the hyperref package. When ' - 'converting to HTML, this defaults to [Abstract, URL, PS, PDF, ' - 'HTML, DVI, TEX, BIB, FTP, HTTP, and RTF].') - self.add_option('--no-link', - action='store_const', - const=[], - dest='link') - - self.set_defaults(titlecase='default') - self.add_option('--titlecase', - type='choice', - choices=['lower', 'upper', 'as-is', 'default'], - help='In the bibliography, force titles into lower-, upper-, or ' - 'title-case. Default: Leave titles unchanged.') - - self.set_defaults(convert='bbl') - self.add_option('--xtx2bib', - action='store_const', - const='bib', - dest='convert', - help='Convert the bibliography information to old-style BibTeX.') - self.add_option('--xtx2html', - action='store_const', - const='html', - dest='convert', - help='Format the bibliography as HTML.') - self.add_option('--bib2xtx', - action='store_const', - const='xtx', - dest='convert', - help='Format the bibliography as HTML.') - - self.add_option('--add-in', - action='store_true', - help='Add "In" for articles.') - self.add_option('--add-proc', - action='store_true', - help='Add "Proc. of" to conference and workshop publications.') - self.set_defaults(add_in=[]) - self.add_option('--add-proceedings', - action='store_true', - help='Add "Proceedings of the" to conference and workshop publications.') - self.add_option('--abstract', - action='store_true', - help='In the bibliography, include paper abstracts if available.') - self.add_option('--no-abstract', - action='store_false', - dest='abstract') - self.add_option('--keywords', - action='store_true', - help='In the bibliography, include paper keywords if available.') - self.add_option('--no-keywords', - action='store_false', - dest='keywords') - self.add_option('--popups', - action='store_true', - help='If abstracts or keywords are to appear for an entry when ' - 'generating HTML, instead hide these extra blocks and reveal them ' - 'as popups when the mouse hovers over the entry.') - self.add_option('--no-popups', - action='store_false') - self.add_option('--title-head', - action='store_true', - help='In the bibliography, put the title bold and first.') - self.add_option('--no-title-head', - action='store_false', - dest='title_head') - self.add_option('--blank-labels', - action='store_true', - help='In the bibliography, leave out item labels.') - self.add_option('--no-blank-labels', - action='store_false', - dest='blank_labels') - self.add_option('--break-lines', - action='store_true', - help='In the bibliography, put author, title, and publication ' - 'information on separate lines.') - self.add_option('--no-break-lines', - action='store_false', - dest='break_lines') - self.add_option('--last-first', - action='store_true', - help='The first name in each author list will appear "Last, First" ' - 'instead of "First Last" (the latter is the default).') - self.add_option('--no-last-first', - action='store_false', - dest='last_first') diff --git a/crosstex/parse.py b/crosstex/parse.py index b91840a..a080cf7 100755 --- a/crosstex/parse.py +++ b/crosstex/parse.py @@ -11,228 +11,172 @@ be resolved and checked. ''' +import collections +import copy +import logging import os -import re import cPickle as pickle -import traceback +import re + import ply.lex import ply.yacc -from copy import copy -from crosstex.options import OptionParser, error, warning - - -class Value: - 'A field value containing a string, number, key, or concatenation.' +import crosstex - def __init__(self, file, line, value, kind=None): - self.file = file - self.line = line - if kind is not None: - self.kind = kind - self.value = value - else: - try: - self.value = int(value) - self.kind = 'number' - except ValueError: - self.value = str(value) - self.kind = 'string' - - def concat(self, other, file, line): - if self.kind != 'concat': - self.value = [Value(self.file, self.line, self.value, self.kind)] - self.kind = 'concat' - self.file = file - self.line = line - if other.kind == 'concat': - self.value.extend(other) - else: - self.value.append(other) - - def resolve(self, db, seen=None, trystrings=False): - entries = [] - if self.kind == 'concat': - for other in self.value: - entries.extend(other.resolve(db, seen)) - elif self.kind == 'key' or (trystrings and self.kind == 'string'): - entry = db._resolve(self.value, seen) - if entry is not None: - entries.append(entry) - self.value = entry - self.kind = 'entry' - elif self.kind == 'key': - error(db.options, '%s:%d: No such key "%s".' % (self.file, self.line, self.value)) - self.kind = 'string' - return entries - - def __str__(self): - if self.kind == 'concat': - return ''.join([str(other) for other in self.value]) - else: - return str(self.value) +logger = logging.getLogger('crosstex.parse') +Entry = collections.namedtuple('Entry', ('kind', 'keys', 'fields', 'file', 'line', 'defaults')) +Value = collections.namedtuple('Value', ('file', 'line', 'kind', 'value')) +Field = collections.namedtuple('Field', ('name', 'value')) +Conditional = collections.namedtuple('Conditional', ('file', 'line', 'if_fields', 'then_fields')) -class Entry: - ''' - A single, raw entry in a database. - - Every piece of information is designed to avoid causing errors in the - lexer/parser, and instead allow errors to be noticed during resolution. - Thus, all fields (fields and defaults) are stored as lists of pairs - rather than dictionaries, in order to preserve duplicates, etc. - ''' - - def __init__(self, kind, keys, fields, file, line, defaults={}): - self.kind = kind - self.keys = keys - self.fields = fields - self.file = file - self.line = line - self.defaults = defaults - +def create_value(_file, line, value, kind=None): + if kind is None: + try: + value = int(value) + kind = 'number' + except ValueError: + value = str(value) + kind = 'string' + return Value(_file, line, kind, value) class XTXFileInfo: 'Same stuff as in Parser, but only for one file' def __init__(self): - self.titlephrases = [] - self.titlesmalls = [] - self.preambles = [] - self.entries = {} + self.titlephrases = set([]) + self.titlesmalls = set([]) + self.preambles = set([]) + self.entries = collections.defaultdict(list) self.tobeparsed = [] def parse(self, file, **kwargs): self.tobeparsed.append(file) def merge(self, db): - db.titlephrases = db.titlephrases + self.titlephrases - db.titlesmalls = db.titlesmalls + self.titlesmalls - db.preambles = db.preambles + self.preambles - db.entries.update(self.entries) + db.titlephrases = set(db.titlephrases) | self.titlephrases + db.titlesmalls = set(db.titlesmalls) | self.titlesmalls + db.preambles = db.preambles | self.preambles + for k, es in self.entries.items(): + db.entries[k] += es for file in self.tobeparsed: db.parse(file, exts=['.xtx', '.bib']) - class Parser: 'A structure of almost raw data from the databases.' - def __init__(self, options, optparser): - self.optparser = optparser - self.options = options - self.titlephrases = [] - self.titlesmalls = [] - self.preambles = [] - self.entries = {} - self.seen = {} - self.cur = [] - - def parse(self, file, exts=['.aux', '.xtx', '.bib']): + def __init__(self, path): + self.titlephrases = set([]) + self.titlesmalls = set([]) + self.preambles = set([]) + self.entries = collections.defaultdict(list) + self.citations = set([]) + self._bibstyle = 'plain' + self._path = path + self._seen = {} + self._dirstack = [] + + def set_path(self, path): + self._path = path + + def parse(self, name, exts=['.aux', '.xtx', '.bbl']): 'Find a file with a reasonable extension and extract its information.' - - filepath, file = os.path.split(file) - file, fileext = os.path.splitext(file) - if fileext in exts: - tryexts = [fileext] + if name in self._seen: + logger.debug('Already processed %r.' % name) + return self._seen[name] + if os.path.sep in name: + logger.debug('%r has a %r, treating it as a path to a file.' % (name, os.path.sep)) + path = name + if self._dirstack: + path = os.path.jaoin(self._dirstack[-1], path) + if not os.path.exists(path): + logger.error('Can not parse %r because it resolves to %r which doesn\'t exist' % (name, path)) + return None + try: + self._dirstack.append(os.path.dirname(path)) + return self._parse_from_path(path) + finally: + self._dirstack.pop() + base, ext = os.path.splitext(name) + if ext: + if not self._check_ext(name, ext, exts): + return None + tryexts = [ext] else: - file += fileext tryexts = exts - - if file in self.seen: - if 'file' in self.options.dump: - print 'Already processed %s.' % file - return self.seen[file] - - if os.path.isabs(filepath): - trydirs = [filepath] + if self._dirstack: + trydirs = self._dirstack[-1:] + self._path else: - trydirs = [os.path.join(dir, filepath) for dir in self.options.dir] - if self.cur: - trydirs.append(os.path.join(self.cur[-1], filepath)) - trydirs.reverse() - - for dir in trydirs: - for ext in tryexts: + trydirs = self._path + for d in trydirs: + for e in tryexts: try: - self.cur.append(dir) - path = os.path.join(dir, file + ext) - try: - stream = open(path, 'r') - except IOError: + self._dirstack.append(d) + path = os.path.join(d, base + e) + if not os.path.exists(path): continue - - if ext == '.aux': - if 'file' in self.options.dump: - print 'Processing auxiliary file %s.' % path - self.parseaux(stream, path) - return path - - self.seen[file] = path - cpath = os.path.join(dir, '.' + file + ext + '.cache') - try: - if os.path.getmtime(path) < os.path.getmtime(cpath): - cstream = open(cpath, 'rb') - if 'file' in self.options.dump: - print "Processing database %s from cache." % path - try: - db = pickle.load(cstream) - cstream.close() - db.merge(self) - return path - except: - if 'file' in self.options.dump: - print "Could not read cache %s, falling back to database." % cpath - except IOError: - pass - except OSError: - pass - - if 'file' in self.options.dump: - print 'Processing database %s.' % path - db = self.parsextx(stream, path) - stream.close() - db.merge(self) - - cpath = os.path.join(dir, '.' + file + ext + '.cache') - try: - cstream = open(cpath, 'wb') - try: - pickle.dump(db, cstream, protocol=2) - except: - if 'file' in self.options.dump: - print "Could not write cache %s." % cpath - cstream.close() - except IOError: - pass - - return path + return self._parse_from_path(path, name=name) finally: - self.cur.pop() - - error(self.options, 'Can not find %s database.' % file) - return file - - def parseaux(self, stream, path): + self._dirstack.pop() + logger.error('Can not find %s database.' % name) + return None + + def _parse_from_path(self, path, name=None): + 'Parse a file from an absolute path' + assert os.path.sep in path + name = name or path + ext = os.path.splitext(path)[1] + if self._check_ext(path, ext): + func = '_parse_ext_' + ext[1:] + self._seen[name] = path + return getattr(self, func)(path) + return None + + def _parse_ext_aux(self, path): 'Parse and handle options set in a TeX .aux file.' - + stream = open(path) + logger.debug('Processing auxiliary file %s.' % path) for line in stream: if line.startswith(r'\citation'): for citation in line[10:].strip().rstrip('}').split(','): citation = citation.strip(',\t') if citation: - self.addopts(['--cite', citation]) + self.citations.add(citation) elif line.startswith(r'\bibstyle'): - self.addopts(['--style'] + line[10:].rstrip().rstrip('}').split(' ')) + self._bibstyle = line[10:].rstrip().rstrip('}').split(' ') elif line.startswith(r'\bibdata'): - self.files(line[9:].rstrip().rstrip('}').split(','), exts=['.bib', '.xtx']) + for f in line[9:].rstrip().rstrip('}').split(','): + self.parse(f, ['.bib', '.xtx']) elif line.startswith(r'\@input'): - self.files(line[8:].rstrip().rstrip('}').split(','), exts=['.aux']) + for f in line[8:].rstrip().rstrip('}').split(','): + self.parse(f, ['.aux']) - def parsextx(self, stream, path): - 'Parse and handle options set in a CrossTeX .xtx or .bib database file.' + def _parse_ext_bib(self, path): + return self._parse_ext_xtx(path) + def _parse_ext_xtx(self, path): + 'Parse and handle options set in a CrossTeX .xtx or BibTeX .bib database file.' + cache_path = os.path.join(os.path.dirname(path), + '.' + os.path.basename(path) + '.cache') + try: + if os.path.exists(cache_path) and \ + os.path.getmtime(path) < os.path.getmtime(cache_path): + logger.debug("Processing database %r from cache." % path) + with open(cache_path, 'rb') as cache_stream: + db = pickle.load(cache_stream) + db.merge(self) + return path + except EOFError as e: + logger.error("Could not read cache '%r', falling back to database: %s." % (cache_path, e)) + except pickle.UnpicklingError as e: + logger.error("Could not read cache '%r', falling back to database: %s." % (cache_path, e)) + except IOError as e: + logger.error("Could not read cache '%r', falling back to database: %s." % (cache_path, e)) + except OSError as e: + logger.error("Could not read cache '%r', falling back to database: %s." % (cache_path, e)) + logger.debug('Processing database %s.' % path) db = XTXFileInfo() - db.options = self.options + stream = open(path) contents = stream.read() if contents: lexer = ply.lex.lex(reflags=re.UNICODE) @@ -242,18 +186,30 @@ def parsextx(self, stream, path): lexer.expectstring = False lexer.db = db - lexer.defaults = {} + lexer.defaults = () parser = ply.yacc.yacc(debug=0, write_tables=0) parser.parse(contents, lexer=lexer) - return db - - def files(self, files, **kwargs): - return [ self.parse(file, **kwargs) for file in files ] - - def addopts(self, opts): - return self.optparser.parse_args(args=opts, values=self.options) - + stream.close() + db.merge(self) + try: + with open(cache_path, 'wb') as cache_stream: + pickle.dump(db, cache_stream, protocol=2) + except pickle.PicklingError as e: + logger.error("Could not write cache '%r', falling back to database: %s." % (cache_path, e)) + except IOError as e: + logger.error("Could not write cache '%r', falling back to database: %s." % (cache_path, e)) + except OSError as e: + logger.error("Could not write cache '%r', falling back to database: %s." % (cache_path, e)) + return path + + def _check_ext(self, name, ext, exts=['.aux', '.xtx', '.bib']): + func = '_parse_ext_' + ext[1:] + if not hasattr(self, func) or not ext in exts: + logger.error('Can not parse %r because the extension is not %s.' + % (name, '/'.join([e[1:] for e in exts]))) + return False + return True # # Tokens @@ -263,7 +219,7 @@ def addopts(self, opts): numberre = re.compile(r'^\d+$') andre = re.compile(r'\s+and\s+') -tokens = ( 'AT', 'COMMA', 'SHARP', 'OPENBRACE', 'CLOSEBRACE', 'LBRACK', +tokens = ( 'AT', 'COMMA', 'OPENBRACE', 'CLOSEBRACE', 'LBRACK', 'RBRACK', 'EQUALS', 'ATINCLUDE', 'ATSTRING', 'ATEXTEND', 'ATPREAMBLE', 'ATCOMMENT', 'ATDEFAULT', 'ATTITLEPHRASE', 'ATTITLESMALL', 'NAME', 'NUMBER', 'STRING', ) @@ -367,11 +323,6 @@ def t_AT(t): t.lexer.expectstring = False return t -def t_SHARP(t): - r'\#' - t.lexer.expectstring = True - return t - def t_COMMA(t): r',' t.lexer.expectstring = False @@ -392,11 +343,10 @@ def t_newline(t): t.lexer.lineno += 1 def t_error(t): - error(t.lexer.db.options, '%s:%d: Syntax error near "%s".' % - (t.lexer.file, t.lexer.lineno, t.value[:20])) + logger.error('%s:%d: Syntax error near "%s".' % + (t.lexer.file, t.lexer.lineno, t.value[:20])) t.skip(1) - # # Grammar; start symbol is stmts. # @@ -412,15 +362,15 @@ def p_ignore(t): def p_stmt_preamble(t): 'stmt : ATPREAMBLE OPENBRACE STRING CLOSEBRACE' - t.lexer.db.preambles.append(t[3]) + t.lexer.db.preambles.add(t[3]) def p_stmt_titlephrase(t): 'stmt : ATTITLEPHRASE STRING' - t.lexer.db.titlephrases.append(t[2]) + t.lexer.db.titlephrases.add(t[2]) def p_stmt_titlesmall(t): 'stmt : ATTITLESMALL STRING' - t.lexer.db.titlesmalls.append(t[2]) + t.lexer.db.titlesmalls.add(t[2]) def p_stmt_include(t): 'stmt : ATINCLUDE NAME' @@ -428,27 +378,27 @@ def p_stmt_include(t): def p_stmt_default(t): 'stmt : ATDEFAULT field' - t.lexer.defaults = copy(t.lexer.defaults) + defaults = dict(t.lexer.defaults) if t[2][1]: - t.lexer.defaults[t[2][0]] = t[2][1] + defaults[t[2][0]] = t[2][1] else: try: - del t.lexer.defaults[t[2][0]] + del defaults[t[2]][0] except KeyError: pass + t.lexer.defaults = tuple(defaults.items()) def p_stmt_string(t): 'stmt : ATSTRING OPENBRACE fields CLOSEBRACE' file, line, defaults = t.lexer.file, t.lineno(2), t.lexer.defaults for key, value in t[3]: - ent = Entry('string', [key], [([], [('name', value)])], - file, line, defaults) - t.lexer.db.entries.setdefault(key, []).append(ent) + ent = Entry('string', [key], [('name', value)], file, line, defaults) + t.lexer.db.entries[key].append(ent) def p_stmt_entry(t): 'stmt : entry' for key in t[1].keys: - t.lexer.db.entries.setdefault(key, []).append(t[1]) + t.lexer.db.entries[key].append(t[1]) def p_entry(t): 'entry : AT NAME OPENBRACE keys COMMA conditionals CLOSEBRACE' @@ -460,81 +410,74 @@ def p_entry_extend(t): entry : ATEXTEND OPENBRACE keys COMMA conditionals CLOSEBRACE | ATEXTEND OPENBRACE keys CLOSEBRACE ''' - try: + if len(t) == 7: fields = t[5] - except IndexError: + else: fields = [] file, line, defaults = t.lexer.file, t.lineno(1), t.lexer.defaults t[0] = Entry('extend', t[3], fields, file, line, defaults) def p_keys_singleton(t): 'keys : NAME' - t[0] = [t[1]] + t[0] = (t[1],) def p_keys_multiple(t): 'keys : NAME EQUALS keys' - t[0] = [t[1]] + t[3] + t[0] = (t[1],) + t[3] def p_conditionals_empty(t): 'conditionals :' - t[0] = [] + t[0] = () def p_conditionals_singleton(t): 'conditionals : conditional' - t[0] = [t[1]] + t[0] = (t[1],) def p_conditionals_multiple(t): 'conditionals : conditional conditionals' - t[0] = [t[1]] + t[2] + t[0] = (t[1],) + t[2] def p_conditionals_unconditional(t): 'conditionals : fields conditionals' - t[0] = [([], t[1])] + t[2] + t[0] = t[1] + t[2] def p_conditional(t): 'conditional : LBRACK fields RBRACK fields' - t[0] = (t[2], t[4]) + t[0] = Conditional(t.lexer.file, t.lineno(1), t[2], t[4]) def p_fields_empty(t): 'fields :' - t[0] = [] + t[0] = () def p_fields_singleton(t): 'fields : field' - t[0] = [t[1]] + t[0] = (t[1],) def p_fields(t): 'fields : field COMMA fields' - t[0] = [t[1]] + t[3] + t[0] = (t[1],) + t[3] def p_field(t): 'field : NAME EQUALS value' - t[0] = (t[1].lower(), t[3]) + t[0] = Field(t[1].lower(), t[3]) def p_value_singleton(t): 'value : simplevalue' t[0] = t[1] -def p_value_concat(t): - 'value : value SHARP simplevalue' - warning(t.lexer.db.options, '%s:%d: The implementation of concatenation is currently broken.' % - (t.lexer.file, t.lexer.lineno, t.value)) - t[0] = t[1] - t[0].concat(t[3]) - def p_simplevalue_name(t): 'simplevalue : NAME' - t[0] = Value(t.lexer.file, t.lineno(1), t[1], 'key') + t[0] = create_value(t.lexer.file, t.lineno(1), t[1], 'key') def p_simplevalue_number(t): 'simplevalue : NUMBER' - t[0] = Value(t.lexer.file, t.lineno(1), t[1]) + t[0] = create_value(t.lexer.file, t.lineno(1), t[1], None) def p_simplevalue_string(t): 'simplevalue : STRING' - t[0] = Value(t.lexer.file, t.lineno(1), t[1]) + t[0] = create_value(t.lexer.file, t.lineno(1), t[1], None) def p_error(t): ply.yacc.errok() - error(t.lexer.db.options, '%s:%d: Parse error near "%s".' % - (t.lexer.file, t.lexer.lineno, t.value[:20])) + logger.error('%s:%d: Parse error near "%s".' % + (t.lexer.file, t.lexer.lineno, t.value[:20])) diff --git a/crosstex/resolve.py b/crosstex/resolve.py deleted file mode 100755 index b917993..0000000 --- a/crosstex/resolve.py +++ /dev/null @@ -1,411 +0,0 @@ -''' -Parse files containing CrossTeX databases and citations. - -Care is taken to ensure that even in the face of errors, CrossTeX does -as much as possible. Additionally, errors are never thrown except when -necessary for the bibliography the user wants. Entries that are not -cited or use in inheritance will never be resolved or touched beyond -the initial parsing, which is designed to be fast and error-free. -This is for efficiency as much as peace of mind. To check every entry -in a database, simply cite every entry, and then they will all have to -be resolved and checked. -''' - -import re -import math -import crosstex.objects - -from copy import copy -from crosstex.options import warning, error - - -class Constraint(dict): - 'A convenience representation of a constrained citation.' - - def __init__(self, key): - parts = key[1:].lower().split(':') - for i in range(len(parts)): - constraint = parts[i].split('=', 1) - if len(constraint) == 1: - try: - values = int(constraint[0]) - except ValueError: - if i == 0: - field = 'author' - else: - field = 'title' - values = constraint[0].split('-') - else: - field = 'year' - values = [str(values)] - else: - field = constraint[0] - values = constraint[1].split('-') - if not field: - error(self.options, 'Empty field in constraint "%s".' % key) - if not values: - error(self.options, 'Empty values in constraint "%s".' % key) - if field and values: - self[field] = values - - def match(self, entry): - for field, values in self.iteritems(): - try: - actual = str(entry.fields[field]).lower() - except KeyError: - return False - for value in values: - if value not in actual: - return False - return True - - -class Database(list): - 'A structure of almost raw data from the databases.' - - def __init__(self, parser): - self.titlephrases = parser.titlephrases - self.titlesmalls = parser.titlesmalls - self.preambles = parser.preambles - self.entries = parser.entries - self.options = parser.options - self.resolved = {} - - semantic = [] - all = not self.options.cite - for key in self.options.cite: - if key == '*': - all = True - elif key.startswith('!'): - semantic.append(key) - else: - entry = self._resolve(key) - if entry is None: - continue - if not entry._citeable: - error(self.options, '%s is not a citeable object.' % key) - elif entry.citation is None: - entry.citation = key - self.append(entry) - elif entry.citation != key: - error(self.options, - 'Citations "%s" and "%s" refer to the same object.' % - (entry.citation, key)) - - entries = {} - if all or semantic: - for key in self.entries: - entry = self._resolve(key) - if entry is not None and entry._citeable: - entries[entry] = True - - for key in semantic: - constraint = Constraint(key) - if not constraint: - error(self.options, 'Empty constraint.') - continue - matches = [entry for entry in entries if constraint.match(entry)] - if not matches: - error(self.options, 'Constraint "%s" has no matches.' % key) - continue - if matches[0].citation is None: - matches[0].citation = key - self.append(matches[0]) - elif matches[0].citation != key: - error(self.options, - 'Citations "%s" and "%s" refer to the same object.' % - (matches[0].citation, key)) - if len(matches) > 1: - error(self.options, 'Ambiguous constraint "%s" has %d matches:' % - (key, len(matches))) - error(self.options, ' accepted key %s.' % (matches[0].keys[0])) - for match in matches[1:]: - error(self.options, ' rejected key %s.' % (match.keys[0])) - - if all: - for entry in entries: - if entry.citation is None: - entry.citation = entry.keys[0] - self.append(entry) - - self._sort() - - def dump(self, kind): - entries = {} - kind = getattr(crosstex.objects, kind) - for key in self.entries: - entry = self._resolve(key) - if entry is not None and isinstance(entry, kind): - entries[str(entry)] = True - return entries.keys() - - def write(self, out, entries=None, headings=None): - 'Print the formatted database to the stream out.' - - if entries is None: - entries = self - if headings is None: - headings = self.options.heading - if (self.options.convert == 'html' or self.options.convert == 'bbl') and len(headings) > 1: - error(self.options, 'The output format does not support multiple heading levels.') - headings = headings[:1] - - if not headings: - if self.options.convert == 'html' or self.options.convert == 'bbl': - max = '' - for entry in entries: - label = entry._format('label') - label = label.replace(r'{\etalchar{+}}', 'X') - if len(label) > len(max): - max = label - if not max: - max = '0' * int(math.ceil(math.log(len(entries) + 1, 10))) - out.write(r'\begin{thebibliography}{' + max + '}\n') - for entry in entries: - out.write(str(entry)) - if self.options.convert == 'html' or self.options.convert == 'bbl': - out.write(r'\end{thebibliography}' + '\n') - return - - heading = headings[0][1] - cats = {} - for entry in entries: - cats.setdefault(str(entry.fields.get(heading, '')), []).append(entry) - - values = [k for k in cats.keys() if k != ''] - values.sort() - if headings[0][0]: - values.reverse() - if '' in cats: - values.insert(0, '') - - for value in values: - if (self.options.convert == 'html' or self.options.convert == 'bbl') and value != '': - out.write(r'{\renewcommand{\refname}{' + str(value) + '}\n') - elif self.options.convert == 'xtx': - default = crosstex.style.common._fieldval(heading, value) - out.write('@default ' + default + ' \n\n') - self.write(out, cats[value], headings[1:]) - if (self.options.convert == 'html' or self.options.convert == 'bbl') and value != '': - out.write('}\n') - - def _sort(self): - citations = self - for reverse, field in self.options.sort: - def keyfunc(x): - value = x._format('sort', field) - if value is None: - return x._format('sort', 'key') - try: - return int(value) - except ValueError: - return value - if reverse: - sortlist = [ (keyfunc(x), len(citations) - i, x) for i, x in enumerate(citations) ] - sortlist.sort() - sortlist.reverse() - else: - sortlist = [ (keyfunc(x), i, x) for i, x in enumerate(citations) ] - sortlist.sort() - citations = [ x[-1] for x in sortlist ] - self[:] = citations - - def _get(self, key): - ''' - Resolve a key to a single entry and all its aliases and extensions. - - This involves looking up each entry and extension of that key, - following out through aliases. Complain if this process reveals - two objects aliased to each other (note that this can happen through - multiple levels of extensions, and need not be immediately obvious). - ''' - - keys = {} - seen = {} - work = {key: True} - base = None - extensions = [] - while work: - key, value = work.popitem() - keys[key] = True - for entry in self.entries.get(key, []): - if entry in seen: - continue - seen[entry] = True - if entry.kind == 'extend': - extensions.append(entry) - elif base is None: - base = entry - else: - error(self.options, '%s:%d: Alias %s is also defined at %s:%d.' % - (base.file, base.line, key, entry.file, entry.line)) - continue - newwork = [k for k in entry.keys if k not in keys] - work.update(dict.fromkeys(newwork, True)) - - if base is None: - if extensions: - error(self.options, '%s is extended but never defined.' % key) - else: - error(self.options, '%s is never defined.' % key) - return (keys.keys(), base, extensions) - - def _resolve(self, key, seen=None): - ''' - Resolve the entry referenced by the given key. - - Evaluate conditional fields, inheritance, @extend entries, etc. until - the entry is stable, and return the result. The resolved entry is - also memoized, so it is efficient to call this twice on one key or - aliased keys. - ''' - - # Return memoized results. - try: - return self.resolved[key] - except KeyError: - pass - - # Find all entries relevant to the given key while guarding against - # inheritance loops. - keys, base, extensions = self._get(key) - if base is None or not keys: - return None - if seen is None: - seen = {} - elif base in seen: - error(self.options, '%s inherits itself recursively.' % key) - return None - seen[base] = True - - # Get the class corresponding to this object type. - try: - kind = getattr(crosstex.objects, base.kind) - except AttributeError: - error(self.options, '%s:%d: There is no such thing as a @%s.\n' % - (base.file, base.line, base.kind)) - return None - if not (isinstance(kind, type) and issubclass(kind, crosstex.objects.Object)): - error(self.options, '%s:%d: There is no such thing as a @%s.\n' % - (base.file, base.line, base.kind)) - return None - - # Resolve fields. Put in conditions a list of lists of conditionals, - # each from an entry. Now is when to complain about duplicated - # fields. Note that the conditions for each entry are flatly - # concatenated into the whole list, in order of preference (extensions - # first, then the base entry, then finally inherited objects in the - # order in which they are assigned). - conditions = [] - for entry in extensions + [base]: - for condition, assignments in entry.fields: - fieldsdict = {} - for field, value in assignments: - field = field.lower() - if field in fieldsdict: - warning(self.options, '%s:%d: %s field is duplicated.' % - (value.file, value.line, field)) - else: - fieldsdict[field] = value - - # Inherit fields, e.g. longname from name. - for field in kind._fillin: - if field in fieldsdict: - continue - for fill in kind._fillin[field]: - if fill in fieldsdict: - fieldsdict[field] = fieldsdict[fill] - break - - conditiondict = {} - for field, value in condition: - field = field.lower() - if field in conditiondict: - warning(self.options, '%s:%d: Conditional %s field is duplicated.' % - (value.file, value.line, field)) - else: - value.resolve(self) - value = str(value) - conditiondict[field] = value - conditions.append((conditiondict, fieldsdict, entry)) - - # Do the resolution, adding inherited entries as they are assigned. - # When the fields stabilize, add in the defaults as a last resort and - # continue until the fields are re-stabilized. - defaulted = False - inherited = {} - fields = {} - i = 0 - while i < len(conditions) or not defaulted: - if i >= len(conditions): - conditions.append(({}, base.defaults, copy(base))) - defaulted = True - i = 0 - - condition, assignments, source = conditions[i] - matches = False - for field, value in condition.iteritems(): - try: - if str(fields[field]) != value: - del conditions[i] - break - except KeyError: - i += 1 - break - else: - matches = True - del conditions[i] - i = 0 - if not matches: - continue - - for field, value in assignments.iteritems(): - # Do not assign to fields that do not exist. - # Complain only if set directly in base or extensions. - if field not in kind._allowed: - if source is base or source in extensions: - warning(self.options, '%s:%d: No such field %s in %s.' % - (base.file, base.line, field, base.kind)) - continue - - # Do not assign fields twice. - if field in fields: - continue - - for entry in value.resolve(self, seen): - if entry not in inherited: - inherited[entry] = True - conditions.append(({}, entry.fields, entry)) - conditions.extend(entry.conditions) - - if field in ['author', 'editor']: - authors = crosstex.objects.ObjectList() - oldcheck = self.options.check - self.options.check = 5 # Ignore everything - for part in re.split('\s+and\s+', str(value)): # XXX split on word boundaries properly - entry = self._resolve(part, seen) - if entry is None: - authors.append(part) - else: - if entry not in inherited: - inherited[entry] = True - conditions.append(({}, entry.fields, entry)) - conditions.extend(entry.conditions) - authors.append(entry) - self.options.check = oldcheck - fields[field] = authors - else: - fields[field] = value - - # Ensure required fields are all provided. - for field in kind._required: - if field not in fields: - error(self.options, '%s:%d: Missing required field %s in %s.' % - (base.file, base.line, field, base.kind)) - - # Memoize the result and return it. - ent = kind(keys, fields, conditions, base.file, base.line) - for key in keys: - self.resolved[key] = ent - - return ent diff --git a/crosstex/style/__init__.py b/crosstex/style/__init__.py index e69de29..98bea76 100644 --- a/crosstex/style/__init__.py +++ b/crosstex/style/__init__.py @@ -0,0 +1,418 @@ +import re + + +class UnsupportedCitation(Exception): + + def __init__(self, citetype): + self.citetype = citetype + + def __str__(self): + return self.citetype + + +class Style(object): + + @classmethod + def formats(cls): + return set([]) + + def __init__(self, flags=None, titlephrases=None, titlesmalls=None): + pass + + def sort_key(self, citation, fields=None): + '''Create a tuple to sort by for the (key, obj) citation''' + raise NotImplementedError() + + def render(self, citations): + '''Render the list of (key, obj) citations''' + raise NotImplementedError() + + def _callback(self, kind): + if not hasattr(self, 'render_' + kind): + return None + else: + return getattr(self, 'render_' + kind) + +################################### Utilities ################################## + +_endre = re.compile(r"(\\end\{[^}]*\}|['\s}])*$") +_protectre = re.compile(r'[\\{}]') + +def punctuate(string, punctuation='', tail=' '): + if string == None: + string = '' + i = _endre.search(string).start() + end = string[i:] + string = string[:i] + if string and not (string.endswith('?') or string.endswith('!') or string.endswith(':') or string.endswith('--') or string.endswith(punctuation)): + string += punctuation + if string or end: + end += tail + return string + end + +################################# Format Names ################################# + +def break_name(name, short=False, plain=False): + '''Break a name into 'first', 'von', 'last', 'jr' parts''' + value = '' + lastchar = ' ' + names = [] + nesting = 0 + assert isinstance(name, str) + for i in range(0, len(name)): + charc = name[i] + if nesting == 0 and lastchar != '\\' and lastchar != ' ' and charc == ' ': + names.append(value) + value = '' + elif lastchar != '\\' and charc == '}': + if not plain: + value += charc + if nesting == 0: + names.append(value) + value = '' + else: + nesting -= 1 + elif lastchar != '\\' and charc == '{': + if not plain: + value += charc + nesting += 1 + elif nesting == 0 and lastchar != '\\' and charc == ',': + pass + else: + if not plain or (charc != '\\' and lastchar != '\\'): + value += charc + lastchar = charc + names.append(value) + + # extract lastname, check suffixes and last name modifiers + # extract also a list of first names + snames = ['Jr.', 'Sr.', 'Jr', 'Sr', 'III', 'IV'] + mnames = ['van', 'von', 'de', 'bin', 'ibn'] + sname = '' + snameoffset = len(names) + while snameoffset > 0 and names[snameoffset - 1] in snames: + snameoffset -= 1 + mnameoffset = 0 + while mnameoffset < snameoffset and names[mnameoffset] not in mnames: + mnameoffset += 1 + lnameoffset = snameoffset + while lnameoffset > 0 and names[lnameoffset - 1] not in mnames: + lnameoffset -= 1 + if lnameoffset <= mnameoffset: + lnameoffset = mnameoffset = snameoffset - 1 + + # return the person info as a tuple + (fnames, mnames, lnames, snames) = (names[:mnameoffset], names[mnameoffset:lnameoffset], names[lnameoffset:snameoffset], names[snameoffset:]) + if short: + fnamesabbr = [] + for n in fnames: + abbr = '' + initial = 0 + sep = '' + while initial < len(n): + if n[initial] == '\\': + initial += 1 + elif n[initial] in '{}': + pass + elif n[initial] == '~': + abbr += n[initial] + elif n[initial] in '-.': + sep = n[initial] + elif sep != None: + if sep != '.': + abbr += sep + abbr += n[initial] + '.' + sep = None + initial += 1 + if abbr: + fnamesabbr.append(abbr) + return (['~'.join(fnamesabbr)], mnames, lnames, snames) + else: + return (fnames, mnames, lnames, snames) + +def name_last_initials(name, size): + (fnames, mnames, lnames, snames) = break_name(name) + mnamestr = '' + for mname in mnames: + first = 0 + while first < len(mname): + if mname[first] not in '{}\\': + mnamestr += mname[first] + break + elif mname[first] == '\\': + first += 2 + else: + first += 1 + lnamestr = '' + for lname in lnames: + if len(lnamestr) >= size: + break + first = 0 + while first < len(lname): + if lname[first] not in '{}\\': + lnamestr += lname[first] + if mnamestr != '' or len(lnamestr) >= size: + break + else: + first += 1 + elif lname[first] == '\\': + first += 2 + else: + first += 1 + return mnamestr + lnamestr + +def name_sort_last_first(name): + (fnames, mnames, lnames, snames) = break_name(name) + fnames = [n.lower().strip(' \t{}') for n in fnames] + mnames = [n.lower().strip(' \t{}') for n in mnames] + lnames = [n.lower().strip(' \t{}') for n in lnames] + snames = [n.lower().strip(' \t{}') for n in snames] + return tuple([tuple(x) for x in ((mnames + lnames), fnames, snames)]) + +def name_last_first(name): + (fnames, mnames, lnames, snames) = break_name(name) + namestr = '' + for n in mnames: + namestr = punctuate(namestr) + n + for n in lnames: + namestr = punctuate(namestr) + n + if len(fnames) > 0: + namestr = punctuate(namestr, ',') + for n in fnames: + namestr = punctuate(namestr) + n + for n in snames: + namestr = punctuate(namestr) + n + return namestr + +def name_first_last(name): + (fnames, mnames, lnames, snames) = break_name(name) + namestr = '' + for n in fnames: + namestr = punctuate(namestr) + n + for n in mnames: + namestr = punctuate(namestr) + n + for n in lnames: + namestr = punctuate(namestr) + n + if len(snames) > 0: + namestr = punctuate(namestr, ',') + for n in snames: + namestr = punctuate(namestr) + n + return namestr + +def name_shortfirst_last(name): + (fnames, mnames, lnames, snames) = break_name(name, short=True) + namestr = '' + for n in fnames: + namestr = punctuate(namestr) + n + for n in mnames: + namestr = punctuate(namestr) + n + for n in lnames: + namestr = punctuate(namestr) + n + if len(snames) > 0: + namestr = punctuate(namestr, ',') + for n in snames: + namestr = punctuate(namestr) + n + return namestr + +def names_last(names): + '''Return just the last names as a tuple''' + if names: + names = list(names) + for i in range(len(names)): + (fnames, mnames, lnames, snames) = break_name(names[i]) + names[i] = '' + for n in lnames: + names[i] = punctuate(names[i]) + n + return tuple(names) + +def names_first_last(names): + '''Make all entries (first, last)''' + names = list(names) + for i in range(len(names)): + names[i] = name_first_last(names[i]) + return tuple(names) + +def names_shortfirst_last(names): + '''Make all entries (first, last)''' + names = list(names) + for i in range(len(names)): + names[i] = name_shortfirst_last(names[i]) + return tuple(names) + +def names_last_first_first_last(names): + '''Make the first entry (last, first), and the rest (first, last)''' + names = list(names) + if len(names) > 0: + names[0] = name_last_first(names[0]) + return tuple(names) + +def names_last_first(names): + '''Make all entries (last, first)''' + names = list(names) + for i in range(len(names)): + names[i] = name_last_first(names[i]) + return tuple(names) + +################################ List Formatters ############################### + +def list_comma_and(objs): + assert all([isinstance(s, str) for s in objs]) + value = '' + for i in range(len(objs)): + if value: + if len(objs) > 2: + value += ',' + value += ' ' + if i == len(objs) - 1: + value += 'and ' + value += str(objs[i]) + return value + +############################### Title Formatters ############################### + +def title_uppercase(title): + newtitle = '' + dollars = 0 + dashlen = 0 + inmath = False + inliteral = False + incommand = False + for i, char in enumerate(title): + if char == '{': + close = _protectre.search(title[i+1:]) + inliteral = not incommand and (close is not None and close.group() == '}') + if char == '}': + inliteral = False + + if char == '\\': + incommand = True + elif char.isspace(): + incommand = False + + if char == '-': + dashlen += 1 + else: + dashlen = 0 + + if char == '$': + dollars += 1 + elif dollars > 0: + inmath = not inmath + dollars = 0 + + if not (inliteral or inmath or incommand): + newtitle += char.upper() + else: + newtitle += char + + if not char.isalnum() and char not in '_\\': + incommand = False + return newtitle + +def title_titlecase(title, titlephrases): + newtitle = '' + ignoreuntil = 0 + dollars = 0 + dashlen = 0 + inmath = False + inliteral = False + incommand = False + wordbreak = True + sentencebreak = True + for i, char in enumerate(title): + if char == '{': + close = _protectre.search(title[i+1:]) + inliteral = not incommand and (close is not None and close.group() == '}') + if char == '}': + inliteral = False + + if char == '\\': + incommand = True + elif char.isspace(): + incommand = False + + if char == '-': + dashlen += 1 + else: + dashlen = 0 + + if char == '$': + dollars += 1 + elif dollars > 0: + inmath = not inmath + dollars = 0 + + if i >= ignoreuntil: + if wordbreak and not (inliteral or inmath or incommand): + match = '' + for phrase in titlephrases: + if title.lower().startswith(phrase.lower(), i) and len(phrase) > len(match) and (i + len(phrase) >= len(title) - 1 or not title[i + len(phrase)].isalnum()): + match = phrase + if len(match) > 0: + ignoreuntil = i + len(match) + newtitle += match + else: + newtitle += char.upper() + else: + newtitle += char + + sentencebreak = (not inliteral and not inmath and not incommand and (char in '!?:.' or dashlen > 1)) or (sentencebreak and (char.isspace() or incommand or inmath or char == '{')) + wordbreak = sentencebreak or (not inliteral and not inmath and not incommand and (char.isspace() or char in ',-')) or (wordbreak and (incommand or inmath or char == '{')) + + if not char.isalnum() and char not in '_\\': + incommand = False + return newtitle + +def title_lowercase(title, lowerphrases): + newtitle = '' + ignoreuntil = 0 + dollars = 0 + dashlen = 0 + inmath = False + inliteral = False + incommand = False + wordbreak = True + sentencebreak = True + for i, char in enumerate(title): + if char == '{': + close = _protectre.search(title[i+1:]) + inliteral = not incommand and (close is not None and close.group() == '}') + if char == '}': + inliteral = False + + if char == '\\': + incommand = True + elif char.isspace(): + incommand = False + + if char == '-': + dashlen += 1 + else: + dashlen = 0 + + if char == '$': + dollars += 1 + elif dollars > 0: + inmath = not inmath + dollars = 0 + + if i >= ignoreuntil: + if wordbreak and not (sentencebreak or inliteral or inmath or incommand): + match = '' + for phrase in lowerphrases: + if title.lower().startswith(phrase.lower(), i) and len(phrase) > len(match) and (i + len(phrase) >= len(title) - 1 or not title[i + len(phrase)].isalnum()): + match = phrase.lower() + if len(match) > 0: + ignoreuntil = i + len(match) + newtitle += match + else: + newtitle += char.lower() + else: + newtitle += char.lower() + + sentencebreak = (not inliteral and not inmath and not incommand and (char in '!?:.' or dashlen > 1)) or (sentencebreak and (char.isspace() or incommand or inmath or char == '{')) + wordbreak = sentencebreak or (not inliteral and not inmath and not incommand and (char.isspace() or char in ',-')) or (wordbreak and (incommand or inmath or char == '{')) + + if not char.isalnum() and char not in '_\\': + incommand = False + return newtitle diff --git a/crosstex/style/abbrv.py b/crosstex/style/abbrv.py deleted file mode 100644 index 0aceb52..0000000 --- a/crosstex/style/abbrv.py +++ /dev/null @@ -1,17 +0,0 @@ -from crosstex.style.basic import * - -# Prefer short names. -string._addproducer(makegetterproducer('longname'), 'value') -string._addproducer(makegetterproducer('shortname'), 'value') - -# Use first initials for authors. -publication._addlistfilter(shortnameslistfilter, 'author') - -# Use numeric labels. -publication._addproducer(emptyproducer, 'label') - -# Emphasize book and journal titles. -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -book._addfilter(emphfilter, 'fulltitle') -journal._addfilter(emphfilter, 'fulltitle') diff --git a/crosstex/style/acm.py b/crosstex/style/acm.py deleted file mode 100755 index 80e77c0..0000000 --- a/crosstex/style/acm.py +++ /dev/null @@ -1,30 +0,0 @@ -from crosstex.style.basic import * - -# Use ACM publication syntax. -publication._addproducer(acmfullpublicationproducer, 'fullpublication') - -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') - -# Use 'In' and emphasize book and journal titles. -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -publication._addfilter(infilter, 'fullpublication', 'booktitle') - -# Use first initials and small caps for authors. -publication._addlistfilter(shortnameslistfilter, 'fullauthor', 'author') -publication._addlistfilter(alllastfirstlistfilter, 'fullauthor', 'author') -publication._addfilter(scfilter, 'fullauthor', 'author') -publication._addlistfilter(shortnameslistfilter, 'fullauthor', 'editor') -publication._addlistfilter(alllastfirstlistfilter, 'fullauthor', 'editor') -publication._addfilter(scfilter, 'fullauthor', 'editor') - -# Sort on last names only. -publication._addlistfilter(alllastlistfilter, 'sort', 'author') -publication._addlistfilter(alllastlistfilter, 'sort', 'editor') -publication._addlistformatter(plainlistformatter, 'sort', 'author') -publication._addlistformatter(plainlistformatter, 'sort', 'editor') - -# Use numeric labels. -publication._addproducer(emptyproducer, 'label') diff --git a/crosstex/style/alpha.py b/crosstex/style/alpha.py deleted file mode 100644 index de897c9..0000000 --- a/crosstex/style/alpha.py +++ /dev/null @@ -1,17 +0,0 @@ -from crosstex.style.basic import * - -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') - -# Use numeric labels. -publication._addproducer(emptyproducer, 'label') - -# Italicize book and journal titles. -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -book._addfilter(emphfilter, 'fulltitle') -journal._addfilter(emphfilter, 'fulltitle') - -# Use alphabetic labels. -publication._addproducer(makegetterproducer('initialslabel'), 'label') diff --git a/crosstex/style/basic.py b/crosstex/style/basic.py deleted file mode 100644 index fd90432..0000000 --- a/crosstex/style/basic.py +++ /dev/null @@ -1,65 +0,0 @@ -from crosstex.style.common import * - -# Standard producers for top-level objects. -publication._addproducer(makejoinproducer(".", "\n\\newblock ", ".", "", 'fullauthor', 'fulltitlepublicationextras'), 'value') -location._addproducer(citystatecountryproducer, 'value') -ObjectList._addlistformatter(commalistformatter, 'value') - -# Long labels. -publication._addproducer(longauthoryearproducer, 'fullnamelabel') # Default to author and year, separated by space. -publication._addfilter(makeuniquefilter(), 'fullnamelabel') # Ensure unique labels by appending letters. -publication._addproducer(makegetterproducer('editor'), 'fullnamelabel', 'author') # Fall back to editor if there are no authors. -publication._addlistformatter(fullnameslistformatter, 'fullnamelabel', 'author') # Format authors as last names. - -# Alphabetic labels. -publication._addproducer(authoryearproducer, 'initialslabel') # Default to author and year with no space between. -publication._addfilter(makeuniquefilter(), 'initialslabel') # Ensure unique labels by appending letters. -publication._addproducer(makegetterproducer('editor'), 'initialslabel', 'author') # Fall back to editor if there are no authors. -publication._addlistformatter(initialslistformatter, 'initialslabel', 'author') # Format authors as a concatenation of last initials. -publication._addfilter(twodigitfilter, 'initialslabel', 'year') # Format year with only two least-significant digits. - -# By default, use key as label if provided. -publication._addproducer(makegetterproducer('key'), 'label') - -# The virtual fullauthor field contains the list of authors (or editors, with no authors). -publication._addproducer(authoreditorproducer, 'fullauthor') - -# The virtual fulltitle field contains the title. -publication._addproducer(makegetterproducer('title'), 'fulltitle') - -# The virtual fullpublication field collects the rest of the relevant information. -publication._addproducer(fullpublicationproducer, 'fullpublication') -publication._addfilter(edfilter, 'fullpublication', 'editor') # Editors are denoted with 'ed.' - -# The virtual fullextras field collects the miscellanous extra information. -publication._addproducer(makejoinproducer(".", " ", ".", "", 'links', 'extras'), 'fullextras') - -# The following are various combinations of useful fields. -publication._addproducer(makejoinproducer(".", "\n\\newblock ", "", "", 'fullauthor', 'fulltitle'), 'fullauthortitle') -publication._addproducer(makejoinproducer(".", "\n\\newblock ", "", "", 'fulltitle', 'fullpublication'), 'fulltitlepublication') -publication._addproducer(makejoinproducer(".", "\n\\newblock ", "", "", 'fullpublication', 'fullextras'), 'fullpublicationextras') -publication._addproducer(makejoinproducer(".", "\n\\newblock ", "", "", 'fulltitle', 'fullpublication', 'fullextras'), 'fulltitlepublicationextras') -publication._addproducer(makejoinproducer(".", "\n\\newblock ", "", "", 'fullauthor', 'fullpublication', 'fullextras'), 'fullauthorpublicationextras') -publication._addproducer(makejoinproducer(".", "\n\\newblock ", "", "", 'fullauthor', 'fulltitle', 'fullpublication'), 'fullauthortitlepublication') - -# Preface conference tracks and workshops with the name of the conference. -conferencetrack._addfilter(conferencetrackfilter, 'value') - -# Specialized publication information. -publication._addproducer(makegetterproducer('howpublished'), 'publication') -url._addproducer(accessedproducer, 'publication') -thesis._addproducer(thesistypeproducer, 'publication') - -# Specialized type information. -patent._addproducer('United States Patent', 'numbertype') -techreport._addproducer('Technical Report', 'numbertype') -rfc._addproducer('IETF Request for Comments', 'numbertype') -mastersthesis._addproducer("Master's", 'thesistype') -phdthesis._addproducer('Ph.D.', 'thesistype') - -# Wrap publications in \bibitem entries. -publication._addfilter(bibitemfilter, 'value') - -# The note field appears at the end of misc objects. -misc._addproducer(makejoinproducer(".", " ", ".", "", 'note', 'links', 'extras'), 'fullextras') -misc._addfilter(killfilter, 'fullpublication', 'note') diff --git a/crosstex/style/bib.py b/crosstex/style/bib.py deleted file mode 100644 index 8e10cd4..0000000 --- a/crosstex/style/bib.py +++ /dev/null @@ -1,28 +0,0 @@ -from crosstex.style.common import * - -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') - -# Standard producers for top-level objects. -publication._addproducer(bibtexproducer, 'value') -location._addproducer(citystatecountryproducer, 'value') -ObjectList._addlistformatter(andlistformatter, 'value') - -# Use empty labels (irrelevant, but labels determine what is 'citeable' and thus produced. -publication._addproducer(emptyproducer, 'label') - -# Preface conference tracks and workshops with the name of the conference. -conferencetrack._addfilter(conferencetrackfilter, 'value') - -# Specialized publication information. -publication._addproducer(makegetterproducer('howpublished'), 'publication') -url._addproducer(accessedproducer, 'publication') -thesis._addproducer(thesistypeproducer, 'publication') - -# Specialized type information. -patent._addproducer('United States Patent', 'numbertype') -techreport._addproducer('Technical Report', 'numbertype') -rfc._addproducer('IETF Request for Comments', 'numbertype') -mastersthesis._addproducer("Master's", 'thesistype') -phdthesis._addfilter('Ph.D.', 'thesistype') diff --git a/crosstex/style/common.py b/crosstex/style/common.py deleted file mode 100755 index 1c39653..0000000 --- a/crosstex/style/common.py +++ /dev/null @@ -1,902 +0,0 @@ -from crosstex.objects import * - -_wordre = re.compile('(-+|\s+|\\\w+|\\\W+|\$[^\$]*\$|[{}])', re.IGNORECASE) -_spacere = re.compile(r'^\s*$') -_specialre = re.compile(r'^(\\.*|\$[^\$]*\$)$') -_punctuationre = re.compile('([:!.?]|-{2,})[\'}\n]*$') -_linkre = re.compile("[a-zA-Z][-+.a-zA-Z0-9]*://([:/?#[\]@!$&'()*+,;=a-zA-Z0-9_\-.~]|%[0-9a-fA-F][0-9a-fA-F]|\\-|\s)*") -_linksub = re.compile('\\-\s') -_protectre = re.compile(r'[\\{}]') -_endre = re.compile(r"(\\end\{[^}]*\}|['\s}])*$") - -_bibtexkinds = ['article', 'book', 'booklet', 'conference', 'inbook', \ - 'incollection', 'inproceedings', 'manual', 'mastersthesis', 'phdthesis', \ - 'proceedings', 'techreport', 'unpublished'] - -# Clean up a string to be a search for the literal -def _sanitize(r): - value = '' - for char in r: - if char == '^': - value += r'\^' - else: - value += '[' + char + ']' - return value - -# Piece an entry together. -def _punctuate(string, punctuation='', tail=' '): - if string == None: - string = '' - i = _endre.search(string).start() - end = string[i:] - string = string[:i] - if string and not (string.endswith('?') or string.endswith('!') or string.endswith(':') or string.endswith('--') or string.endswith(punctuation)): - string += punctuation - if string or end: - end += tail - return string + end - -def _names(name, short=False, plain=False): - value = '' - lastchar = ' ' - names = [] - nesting = 0 - if isinstance(name, Formatter): - name = name._format('value') or '' - for i in range(0, len(name)): - charc = name[i] - if nesting == 0 and lastchar != '\\' and lastchar != ' ' and charc == ' ': - names.append(value) - value = '' - elif lastchar != '\\' and charc == '}': - if not plain: - value += charc - if nesting == 0: - names.append(value) - value = '' - else: - nesting -= 1 - elif lastchar != '\\' and charc == '{': - if not plain: - value += charc - nesting += 1 - elif nesting == 0 and lastchar != '\\' and charc == ',': - pass - else: - if not plain or (charc != '\\' and lastchar != '\\'): - value += charc - lastchar = charc - names.append(value) - - # extract lastname, check suffixes and last name modifiers - # extract also a list of first names - snames = ['Jr.', 'Sr.', 'Jr', 'Sr', 'III', 'IV'] - mnames = ['van', 'von', 'de', 'bin', 'ibn'] - sname = '' - snameoffset = len(names) - while snameoffset > 0 and names[snameoffset - 1] in snames: - snameoffset -= 1 - mnameoffset = 0 - while mnameoffset < snameoffset and names[mnameoffset] not in mnames: - mnameoffset += 1 - lnameoffset = snameoffset - while lnameoffset > 0 and names[lnameoffset - 1] not in mnames: - lnameoffset -= 1 - if lnameoffset <= mnameoffset: - lnameoffset = mnameoffset = snameoffset - 1 - - # return the person info as a tuple - (fnames, mnames, lnames, snames) = (names[:mnameoffset], names[mnameoffset:lnameoffset], names[lnameoffset:snameoffset], names[snameoffset:]) - if short: - fnamesabbr = [] - for n in fnames: - abbr = '' - initial = 0 - sep = '' - while initial < len(n): - if n[initial] == '\\': - initial += 1 - elif n[initial] in '{}': - pass - elif n[initial] == '~': - abbr += n[initial] - elif n[initial] in '-.': - sep = n[initial] - elif sep != None: - if sep != '.': - abbr += sep - abbr += n[initial] + '.' - sep = None - initial += 1 - if abbr: - fnamesabbr.append(abbr) - return (['~'.join(fnamesabbr)], mnames, lnames, snames) - else: - return (fnames, mnames, lnames, snames) - -def _last_initials(name, size): - (fnames, mnames, lnames, snames) = _names(name) - mnamestr = '' - for mname in mnames: - first = 0 - while first < len(mname): - if mname[first] not in '{}\\': - mnamestr += mname[first] - break - elif mname[first] == '\\': - first += 2 - else: - first += 1 - lnamestr = '' - for lname in lnames: - if len(lnamestr) >= size: - break - first = 0 - while first < len(lname): - if lname[first] not in '{}\\': - lnamestr += lname[first] - if mnamestr != '' or len(lnamestr) >= size: - break - else: - first += 1 - elif lname[first] == '\\': - first += 2 - else: - first += 1 - return mnamestr + lnamestr - -def _fieldval(field, value): - if isinstance(value, Object): - return '%s = %s' % (field, value.citation) - else: - value = str(value) - try: - return '%s = %d' % (field, int(value)) - except: - if '{' in value or '}' in value: - return '%s = {%s}' % (field, value) - else: - return '%s = "%s"' % (field, value) - -def makegetterproducer(field): - def getterproducer(obj, value, context): - return obj._format(*(context + (field,))) - return getterproducer - -def makejoinproducer(punctuation, space, final, finalspace, *fields): - def joinproducer(obj, value, context): - value = '' - for field in fields: - fieldvalue = obj._format(*(context + (field,))) - if fieldvalue: - value = _punctuate(value, punctuation, space) + fieldvalue - return _punctuate(value, final, finalspace) - return joinproducer - -def bibtexproducer(obj, value, context): - kind = obj.kind - if kind not in _bibtexkinds: - kind = 'misc' - value = '@%s{%s' % (kind, obj.citation) - for field in obj.fields: - fieldvalue = obj._format(*(context + (field,))) - if fieldvalue: - value += ',\n\t' + _fieldval(field, fieldvalue) - value += '}\n\n' - return value - -def crosstexproducer(obj, value, context): - if not obj.keys: - return '' - value = '@%s{%s' % (obj.kind, ' = '.join(obj.keys)) - for field in obj.fields: - value += ',\n\t' + _fieldval(field, obj.fields[field]) - value += '}\n\n' - return value - -def makelinksproducer(fields): - def linksproducer(obj, value, context): - links = '' - for field in fields: - myfield = field.lower() - fieldvalue = obj._format(*(context + (myfield,))) - if fieldvalue: - for m in _linkre.finditer(str(fieldvalue)): - uri = m.group() - _linksub.sub(uri, '') - links = _punctuate(links) + '\\href{%s}{\\small\\textsc{%s}}' % (uri, field) - return links - return linksproducer - -def extrasproducer(obj, value, context): - extras = '' - abstractvalue = obj._format(*(context + ('abstract',))) - keywordsvalue = obj._format(*(context + ('keywords',))) - if abstractvalue: - extras = _punctuate(extras, '\n', tail='') + '\\noindent\\begin{small}%s\\end{small}' % abstractvalue - if keywordsvalue: - extras = _punctuate(extras, '\n\n', tail='') + '\\noindent\\begin{small}\\textsc{Keywords:} %s\\end{small}' % keywordsvalue - if extras: - extras = '\\begin{quotation}' + extras + '\\end{quotation}' - return extras - -def authoryearproducer(obj, value, context): - authorvalue = obj._format(*(context + ('author',))) - yearvalue = obj._format(*(context + ('year',))) - if yearvalue == None: - yearvalue = '' - else: - yearvalue = str(yearvalue) - if authorvalue == None: - authorvalue = '' - else: - authorvalue = str(authorvalue) - return authorvalue + yearvalue or None - -def longauthoryearproducer(obj, value, context): - authorvalue = obj._format(*(context + ('author',))) - yearvalue = obj._format(*(context + ('year',))) - if yearvalue == None: - yearvalue = '' - else: - yearvalue = str(yearvalue) - if authorvalue == None: - authorvalue = '' - else: - authorvalue = str(authorvalue) - return _punctuate(authorvalue) + yearvalue or None - -def authoreditorproducer(obj, value, context): - authorvalue = obj._format(*(context + ('author',))) - if authorvalue == None: - authorvalue = obj._format(*(context + ('editor',))) - if authorvalue == None: - return None - if ' and ' in authorvalue: - authorvalue = str(authorvalue) + ', eds.' - else: - authorvalue = str(authorvalue) + ', ed.' - else: - authorvalue = str(authorvalue) - return authorvalue - -def dateproducer(obj, value, context): - value = '' - monthvalue = obj._format(*(context + ('month',))) - if monthvalue: - value = _punctuate(value, ',') + str(monthvalue) - yearvalue = obj._format(*(context + ('year',))) - if yearvalue: - value = _punctuate(value, ',') + str(yearvalue) - return value - -def fullpublicationproducer(obj, value, context): - value = obj._format(*(context + ('publication',))) - booktitlevalue = obj._format(*(context + ('booktitle',))) - journalvalue = obj._format(*(context + ('journal',))) - if booktitlevalue: - value = _punctuate(value, '.') + str(booktitlevalue) - volumevalue = obj._format(*(context + ('volume',))) - if volumevalue: - value += ', volume %s' % volumevalue - seriesvalue = obj._format(*(context + ('series',))) - if seriesvalue : - value += ' of \\emph{%s}' % seriesvalue - chaptervalue = obj._format(*(context + ('chapter',))) - if chaptervalue: - value += ', chapter %s' % chaptervalue - elif journalvalue: - value = _punctuate(value, ',') + str(journalvalue) - numbervalue = obj._format(*(context + ('number',))) - volumevalue = obj._format(*(context + ('volume',))) - pagesvalue = obj._format(*(context + ('pages',))) - if numbervalue or volumevalue or pagesvalue: - value = _punctuate(value, ',') - if volumevalue: - value += str(volumevalue) - if numbervalue: - value += '(%s)' % numbervalue - if pagesvalue: - if volumevalue or numbervalue: - value += ':%s' % pagesvalue - elif pagesvalue : - value += 'page %s' % pagesvalue - else: - try: - pagenum = int(pagesvalue) - value += 'page %d' % pagenum - except ValueError: - value += 'pages %s' % pagesvalue - institutionvalue = obj._format(*(context + ('institution',))) - if institutionvalue: - value = _punctuate(value, ',') + str(institutionvalue) - schoolvalue = obj._format(*(context + ('school',))) - if schoolvalue: - value = _punctuate(value, ',') + str(schoolvalue) - if not journalvalue: - numbervalue = obj._format(*(context + ('number',))) - if numbervalue: - value = _punctuate(value, ',') - numbertypevalue = obj._format(*(context + ('numbertype',))) - if numbertypevalue: - value = _punctuate(value + str(numbertypevalue)) - value += str(numbervalue) - pagesvalue = obj._format(*(context + ('pages',))) - if pagesvalue: - try: - pagenum = int(pagesvalue) - value = _punctuate(value, ',') + ('page %d' % pagenum) - except ValueError: - value = _punctuate(value, ',') + ('pages %s' % pagesvalue) - authorvalue = obj._format(*(context + ('author',))) - editorvalue = obj._format(*(context + ('editor',))) - if authorvalue and editorvalue: - value = _punctuate(value, ',') + str(editorvalue) - publishervalue = obj._format(*(context + ('publisher',))) - if publishervalue: - value = _punctuate(value, ',') + str(publishervalue) - addressvalue = obj._format(*(context + ('address',))) - if addressvalue: - value = _punctuate(value, ',') + str(addressvalue) - notevalue = obj._format(*(context + ('note',))) - if notevalue: - value = _punctuate(value, ',') + str(notevalue) - yearvalue = obj._format(*(context + ('year',))) - if yearvalue: - value = _punctuate(value, ',') - monthvalue = obj._format(*(context + ('month',))) - if monthvalue: - value = _punctuate(value + str(monthvalue)) - value += str(yearvalue) - return value - -def acmfullpublicationproducer(obj, value, context): - value = obj._format(*(context + ('publication',))) - booktitlevalue = obj._format(*(context + ('booktitle',))) - journalvalue = obj._format(*(context + ('journal',))) - if booktitlevalue: - value = _punctuate(value, '.') + str(booktitlevalue) - volumevalue = obj._format(*(context + ('volume',))) - if volumevalue: - value += ', vol. %s' % volumevalue - seriesvalue = obj._format(*(context + ('series',))) - if seriesvalue : - value += ' of \\emph{%s}' % seriesvalue - chaptervalue = obj._format(*(context + ('chapter',))) - if chaptervalue: - value += ', chap. %s' % chaptervalue - elif journalvalue: - value = _punctuate(value, ',') + str(journalvalue) - volumevalue = obj._format(*(context + ('volume',))) - if volumevalue: - value = _punctuate(value) + '\\emph{%s}' % str(volumevalue) - numbervalue = obj._format(*(context + ('number',))) - if numbervalue: - value = _punctuate(value, ',') + str(numbervalue) - - addryearvalue = '' - addressvalue = obj._format(*(context + ('address',))) - if addressvalue: - addryearvalue = _punctuate(addryearvalue, ',') + str(addressvalue) - datevalue = '' - monthvalue = obj._format(*(context + ('month',))) - if monthvalue: - datevalue = _punctuate(datevalue) + str(monthvalue) - yearvalue = obj._format(*(context + ('year',))) - if yearvalue: - datevalue = _punctuate(datevalue) + str(yearvalue) - if datevalue: - addryearvalue = _punctuate(addryearvalue, ',') + str(datevalue) - if addryearvalue: - value = _punctuate(value) + '(%s)' % addryearvalue - - institutionvalue = obj._format(*(context + ('institution',))) - if institutionvalue: - value = _punctuate(value, ',') + str(institutionvalue) - schoolvalue = obj._format(*(context + ('school',))) - if schoolvalue: - value = _punctuate(value, ',') + str(schoolvalue) - - publishervalue = obj._format(*(context + ('publisher',))) - if publishervalue: - value = _punctuate(value, ',') + str(publishervalue) - - authorvalue = obj._format(*(context + ('author',))) - editorvalue = obj._format(*(context + ('editor',))) - if authorvalue and editorvalue: - value = _punctuate(value, ',') + str(editorvalue) - - pagesvalue = obj._format(*(context + ('pages',))) - if pagesvalue: - if not journalvalue: - try: - pagenum = int(pagesvalue) - pagesvalue = 'p. ' + pagenum - except ValueError: - pagesvalue = 'pp. ' + pagesvalue - value = _punctuate(value, ',') + str(pagesvalue) - notevalue = obj._format(*(context + ('note',))) - if notevalue: - value = _punctuate(value, ',') + str(notevalue) - return value - -def accessedproducer(obj, value, context): - urlvalue = str(obj._format(*(context + ('url',)))) - dayvalue = obj._format(*(context + ('accessday',))) - yearvalue = obj._format(*(context + ('accessyear',))) - monthvalue = obj._format(*(context + ('accessmonth',))) - if yearvalue or monthvalue: - urlvalue = _punctuate(urlvalue, ',') + 'Accessed' - if monthvalue: - urlvalue = _punctuate(urlvalue) + monthvalue - if dayvalue: - urlvalue = _punctuate(urlvalue) + dayvalue - if yearvalue: - urlvalue = _punctuate(urlvalue, ',') + yearvalue - elif yearvalue: - urlvalue = _punctuate(urlvalue) + yearvalue - return urlvalue - -def citystatecountryproducer(obj, value, context): - cityvalue = obj._format(*(context + ('city',))) - statevalue = obj._format(*(context + ('state',))) - countryvalue = obj._format(*(context + ('country',))) - value = '' - if cityvalue: - value = _punctuate(value, ',') + str(cityvalue) - if statevalue: - value = _punctuate(value, ',') + str(statevalue) - if countryvalue: - value = _punctuate(value, ',') + str(countryvalue) - return value - -def thesistypeproducer(obj, value, context): - typevalue = obj._format(*(context + ('type',))) - if typevalue: - return str(typevalue) - typevalue = obj._format(*(context + ('thesistype',))) - if typevalue: - return _punctuate(typevalue) + 'Thesis' - return None - -def emptyproducer(obj, value, context): - return '' - -def lastfirstfilter(obj, objvalue, context): - (fnames, mnames, lnames, snames) = _names(objvalue) - namestr = '' - for n in mnames: - namestr = _punctuate(namestr) + n - for n in lnames: - namestr = _punctuate(namestr) + n - if len(fnames) > 0: - namestr = _punctuate(namestr, ',') - for n in fnames: - namestr = _punctuate(namestr) + n - for n in snames: - namestr = _punctuate(namestr) + n - return namestr - -def shortnamesfilter(obj, objvalue, context): - (fnames, mnames, lnames, snames) = _names(objvalue, short=True) - namestr = '' - for n in fnames: - namestr = _punctuate(namestr) + n - for n in mnames: - namestr = _punctuate(namestr) + n - for n in lnames: - namestr = _punctuate(namestr) + n - if len(snames) > 0: - namestr = _punctuate(namestr, ',') - for n in snames: - namestr = _punctuate(namestr) + n - return namestr - -def shortnameslistfilter(obj, objvalue, context): - for i in range(len(objvalue)): - objvalue[i] = shortnamesfilter(obj, objvalue[i], context) - return objvalue - -def lastfirstlistfilter(obj, objvalue, context): - if objvalue: - objvalue = copy(objvalue) - objvalue[0] = lastfirstfilter(obj, objvalue[0], context) - return objvalue - -def alllastfirstlistfilter(obj, objvalue, context): - if objvalue: - objvalue = copy(objvalue) - for i in range(len(objvalue)): - objvalue[i] = lastfirstfilter(obj, objvalue[i], context) - return objvalue - -def alllastlistfilter(obj, objvalue, context): - if objvalue: - objvalue = copy(objvalue) - for i in range(len(objvalue)): - (fnames, mnames, lnames, snames) = _names(objvalue[i]) - objvalue[i] = '' - for n in lnames: - objvalue[i] = _punctuate(objvalue[i]) + n - return objvalue - -def plainlistformatter(obj, objvalue, context): - value = '' - for i in range(len(objvalue)): - value = _punctuate(value, ',') + str(objvalue[i]) - return value - -def commalistformatter(obj, objvalue, context): - value = '' - for i in range(len(objvalue)): - if value: - if len(objvalue) > 2: - value += ',' - value += ' ' - if i == len(objvalue) - 1: - value += 'and ' - value += str(objvalue[i]) - return value - -def andlistformatter(obj, objvalue, context): - return ' and '.join([str(element) for element in objvalue]) - -def andcrosstexlistformatter(obj, objvalue, context): - return ' and '.join([isinstance(element, Object) and element._primarykey or str(element) for element in objvalue]) - -def initialslistformatter(obj, objvalue, context): - value = '' - if len(objvalue) == 1: - value = _last_initials(objvalue[0], 3) - elif len(objvalue) <= 4: - for i in range(0, min(len(objvalue), 5)): - value += _last_initials(objvalue[i], 1) - else: - for i in range(0, 4): - value += _last_initials(objvalue[i], 1) - value += '{\etalchar{+}}' - return value - -def fullnameslistformatter(obj, objvalue, context): - value = '' - if len(objvalue) == 2: - (fnames1, mnames1, lnames1, snames1) = _names(objvalue[0]) - (fnames2, mnames2, lnames2, snames2) = _names(objvalue[1]) - value = ' '.join(mnames1 + lnames1) + ' \& ' + ' '.join(mnames2 + lnames2) - elif objvalue: - (fnames1, mnames1, lnames1, snames1) = _names(objvalue[0]) - value = ' '.join(mnames1 + lnames1) - if len(objvalue) > 2: - value += ' et al.' - return value - -def makebracketfilter(left, right): - def bracketfilter(obj, objvalue, context): - if objvalue: - return '%s%s%s' % (left, objvalue.strip(), right) - return objvalue - return bracketfilter - -def makesuffixfilter(suffix): - def suffixfilter(obj, objvalue, context): - if objvalue: - return '%s%s' % (objvalue.strip(), suffix) - return objvalue - return suffixfilter - -def edfilter(obj, objvalue, context): - if objvalue: - if ' and ' in objvalue: - objvalue = objvalue + ', eds.' - else: - objvalue = objvalue + ', ed.' - return objvalue - -def makeprefixfilter(prefix): - def prefixfilter(obj, objvalue, context): - if objvalue: - return '%s%s' % (prefix, objvalue.strip()) - return objvalue - return prefixfilter - -def bibitemfilter(obj, objvalue, context): - if objvalue: - label = obj._format(*(context + ('label',))) - if label: - label = '[%s]' % label - return '\\bibitem%s{%s}\n%s\n\n' % (label, obj.citation, objvalue.strip()) - return objvalue - -def emptyfilter(obj, objvalue, context): - return '' - -def makeuniquefilter(): - used = [] - def uniquefilter(obj, objvalue, context): - if objvalue != '': - if objvalue in used: - for char in list('abcdefghijklmnopqrstuvwxyz'): - if objvalue + char not in used: - objvalue += char - break - else: - raise ValueError, 'too many citations with key %s' % objvalue - used.append(objvalue) - return objvalue - return uniquefilter - -def twodigitfilter(obj, objvalue, context): - return objvalue[-2:] - -infilter = makeprefixfilter('In ') -procfilter = makeprefixfilter('Proc. of ') -proceedingsfilter = makeprefixfilter('Proceedings of the ') -emphfilter = makebracketfilter('\\emph{', '}') -boldfilter = makebracketfilter('\\textbf{', '}') -scfilter = makebracketfilter('\\textsc{', '}') -bracesfilter = makebracketfilter('{', '}') -quotefilter = makebracketfilter("``", "''") - -def conferencetrackfilter(obj, objvalue, context): - value = obj._format(*(context + ('conference',))) - value = _punctuate(value, ',') + objvalue - return value - -def killfilter(obj, objvalue, context): - if context[-1] in obj._required: - return objvalue - else: - return '' - -def titlecasefilter(obj, objvalue, context): - newtitle = '' - dollars = 0 - dashlen = 0 - inmath = False - inliteral = False - incommand = False - wordbreak = True - sentencebreak = True - for i, char in enumerate(objvalue): - if char == '{': - close = _protectre.search(objvalue[i+1:]) - inliteral = not incommand and (close is not None and close.group() == '}') - if char == '}': - inliteral = False - - if char == '\\': - incommand = True - elif char.isspace(): - incommand = False - - if char == '-': - dashlen += 1 - else: - dashlen = 0 - - if char == '$': - dollars += 1 - elif dollars > 0: - inmath = not inmath - dollars = 0 - - if not (inliteral or inmath or incommand): - if wordbreak: - newtitle += char.upper() - else: - newtitle += char.lower() - else: - newtitle += char - - sentencebreak = (not inliteral and not inmath and not incommand and (char in '!?:.' or dashlen > 1)) or (sentencebreak and (char.isspace() or incommand or inmath or char == '{')) - wordbreak = sentencebreak or (not inliteral and not inmath and not incommand and (char.isspace() or char in ',-')) or (wordbreak and (incommand or inmath or char == '{')) - - if not char.isalnum() and char not in '_\\': - incommand = False - return newtitle - -def lowertitlecasefilter(obj, objvalue, context): - newtitle = '' - dollars = 0 - dashlen = 0 - inmath = False - inliteral = False - incommand = False - sentencebreak = True - for i, char in enumerate(objvalue): - if char == '{': - close = _protectre.search(objvalue[i+1:]) - inliteral = not incommand and (close is not None and close.group() == '}') - if char == '}': - inliteral = False - - if char == '\\': - incommand = True - elif char.isspace(): - incommand = False - - if char == '-': - dashlen += 1 - else: - dashlen = 0 - - if char == '$': - dollars += 1 - elif dollars > 0: - inmath = not inmath - dollars = 0 - - if not (inliteral or inmath or incommand): - if sentencebreak: - newtitle += char.upper() - else: - newtitle += char.lower() - else: - newtitle += char - - sentencebreak = (not inliteral and not inmath and not incommand and (char in '!?:.' or dashlen > 1)) or (sentencebreak and (char.isspace() or incommand or inmath or char == '{')) - - if not char.isalnum() and char not in '_\\': - incommand = False - return newtitle - -def uppercasefilter(obj, objvalue, context): - newtitle = '' - dollars = 0 - dashlen = 0 - inmath = False - inliteral = False - incommand = False - for i, char in enumerate(objvalue): - if char == '{': - close = _protectre.search(objvalue[i+1:]) - inliteral = not incommand and (close is not None and close.group() == '}') - if char == '}': - inliteral = False - - if char == '\\': - incommand = True - elif char.isspace(): - incommand = False - - if char == '-': - dashlen += 1 - else: - dashlen = 0 - - if char == '$': - dollars += 1 - elif dollars > 0: - inmath = not inmath - dollars = 0 - - if not (inliteral or inmath or incommand): - newtitle += char.upper() - else: - newtitle += char - - if not char.isalnum() and char not in '_\\': - incommand = False - return newtitle - -def maketitlephrasefilter(titlephrases): - def titlephrasefilter(obj, objvalue, context): - newtitle = '' - ignoreuntil = 0 - dollars = 0 - dashlen = 0 - inmath = False - inliteral = False - incommand = False - wordbreak = True - sentencebreak = True - for i, char in enumerate(objvalue): - if char == '{': - close = _protectre.search(objvalue[i+1:]) - inliteral = not incommand and (close is not None and close.group() == '}') - if char == '}': - inliteral = False - - if char == '\\': - incommand = True - elif char.isspace(): - incommand = False - - if char == '-': - dashlen += 1 - else: - dashlen = 0 - - if char == '$': - dollars += 1 - elif dollars > 0: - inmath = not inmath - dollars = 0 - - if i >= ignoreuntil: - if wordbreak and not (inliteral or inmath or incommand): - match = '' - for phrase in titlephrases: - if objvalue.lower().startswith(phrase.lower(), i) and len(phrase) > len(match) and (i + len(phrase) >= len(objvalue) - 1 or not objvalue[i + len(phrase)].isalnum()): - match = phrase - if len(match) > 0: - ignoreuntil = i + len(match) - newtitle += match - else: - newtitle += char - else: - newtitle += char - - sentencebreak = (not inliteral and not inmath and not incommand and (char in '!?:.' or dashlen > 1)) or (sentencebreak and (char.isspace() or incommand or inmath or char == '{')) - wordbreak = sentencebreak or (not inliteral and not inmath and not incommand and (char.isspace() or char in ',-')) or (wordbreak and (incommand or inmath or char == '{')) - - if not char.isalnum() and char not in '_\\': - incommand = False - return newtitle - return titlephrasefilter - -def makelowerphrasefilter(lowerphrases): - def lowerphrasefilter(obj, objvalue, context): - newtitle = '' - ignoreuntil = 0 - dollars = 0 - dashlen = 0 - inmath = False - inliteral = False - incommand = False - wordbreak = True - sentencebreak = True - for i, char in enumerate(objvalue): - if char == '{': - close = _protectre.search(objvalue[i+1:]) - inliteral = not incommand and (close is not None and close.group() == '}') - if char == '}': - inliteral = False - - if char == '\\': - incommand = True - elif char.isspace(): - incommand = False - - if char == '-': - dashlen += 1 - else: - dashlen = 0 - - if char == '$': - dollars += 1 - elif dollars > 0: - inmath = not inmath - dollars = 0 - - if i >= ignoreuntil: - if wordbreak and not (sentencebreak or inliteral or inmath or incommand): - match = '' - for phrase in lowerphrases: - if objvalue.lower().startswith(phrase.lower(), i) and len(phrase) > len(match) and (i + len(phrase) >= len(objvalue) - 1 or not objvalue[i + len(phrase)].isalnum()): - match = phrase.lower() - if len(match) > 0: - ignoreuntil = i + len(match) - newtitle += match - else: - newtitle += char - else: - newtitle += char - - sentencebreak = (not inliteral and not inmath and not incommand and (char in '!?:.' or dashlen > 1)) or (sentencebreak and (char.isspace() or incommand or inmath or char == '{')) - wordbreak = sentencebreak or (not inliteral and not inmath and not incommand and (char.isspace() or char in ',-')) or (wordbreak and (incommand or inmath or char == '{')) - - if not char.isalnum() and char not in '_\\': - incommand = False - return newtitle - return lowerphrasefilter - -def listproducer(obj, value, context): - if isinstance(obj, list): - return list(obj) - else: - return None - -ObjectList._addproducer(listproducer, 'value') -Object._addlistfilter(alllastfirstlistfilter, 'sort', 'author') -Object._addfilter(titlecasefilter, 'sort', 'author') diff --git a/crosstex/style/full.py b/crosstex/style/full.py deleted file mode 100644 index 7e49aa4..0000000 --- a/crosstex/style/full.py +++ /dev/null @@ -1,19 +0,0 @@ -from crosstex.style.basic import * - -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') - -# Preface conference names with 'Proceedings of the'. -publication._addfilter(proceedingsfilter, 'fullpublication', 'booktitle') - -# Use 'In' and emphasize book and journal titles. -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -book._addfilter(emphfilter, 'fulltitle') -journal._addfilter(emphfilter, 'fulltitle') -publication._addfilter(infilter, 'fullpublication', 'booktitle') -publication._addfilter(infilter, 'fullpublication', 'journal') - -# Use long labels. -publication._addproducer(makegetterproducer('fullnamelabel'), 'label') diff --git a/crosstex/style/html.py b/crosstex/style/html.py deleted file mode 100644 index f9a22c8..0000000 --- a/crosstex/style/html.py +++ /dev/null @@ -1,26 +0,0 @@ -from crosstex.style.basic import * - -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') - -# Preface conference names with 'Proceedings of the'. -publication._addfilter(proceedingsfilter, 'fullpublication', 'booktitle') - -# Use 'In' and emphasize book and journal titles. -publication._addfilter(infilter, 'fullpublication', 'booktitle') -publication._addfilter(infilter, 'fullpublication', 'journal') -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -book._addfilter(emphfilter, 'fulltitle') -journal._addfilter(emphfilter, 'fulltitle') - -# Use long labels. -publication._addproducer(makegetterproducer('fullnamelabel'), 'label') - -# Show extras and wrap in some HTML magic to allow popups. -publication._addproducer(extrasproducer, 'extras') - -# Title first and bold. -publication._addfilter(boldfilter, 'fulltitle') -publication._addproducer(makejoinproducer(".", "\n\\newblock ", ".", "", 'fulltitle', 'fullauthorpublicationextras'), 'value') diff --git a/crosstex/style/plain.py b/crosstex/style/plain.py index 7066353..880b81e 100644 --- a/crosstex/style/plain.py +++ b/crosstex/style/plain.py @@ -1,14 +1,317 @@ -from crosstex.style.basic import * +import math -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') +import crosstex.style -# Use numeric labels. -publication._addproducer(emptyproducer, 'label') -# Use 'In' and emphasize book and journal titles. -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -book._addfilter(emphfilter, 'fulltitle') -journal._addfilter(emphfilter, 'fulltitle') +class Style(crosstex.style.Style): + + @classmethod + def formats(cls): + return set(['bbl']) + + def __init__(self, flags=None, titlephrases=None, titlesmalls=None): + self._titlephrases = titlephrases or set([]) + self._titlesmalls = titlesmalls or set([]) + self._flags = flags or set([]) + + def sort_key(self, citation, fields=None): + if fields is not None: # XXX + raise NotImplementedError() + cite, obj = citation + author = None + if 'author' in obj.allowed and obj.author: + author = [a.name.value if hasattr(a, 'name') else a.value for a in obj.author] + author = [crosstex.style.name_sort_last_first(a) for a in author] + author = tuple(author) + title = None + if 'title' in obj.allowed and obj.title: + title = obj.title.value + where = None + if 'booktitle' in obj.allowed and obj.booktitle: + where = self.render_booktitle(obj.booktitle) + elif 'journal' in obj.allowed and obj.journal: + where = self.render_journal(obj.journal) + when = None + if 'year' in obj.allowed and obj.year: + when = str(obj.year.value) + return author, title, where, when + + def render(self, citations): + num = int(math.log10(len(citations))) + 1 if citations else 1 + bib = '\\newcommand{\etalchar}[1]{$^{#1}$}\n' + bib += '\\begin{thebibliography}{%s}\n' % ('0' * num) + for cite, obj in citations: + cb = self._callback(obj.kind) + if cb is None: + raise crosstex.style.UnsupportedCitation(obj.kind) + item = cb(obj) + bib += '\n' + self.bibitem(cite, item) + bib += '\n\end{thebibliography}\n' + return bib + + # Stuff to override for other formats + + def bibitem(self, cite, item): + return ('\\bibitem{%s}\n' % cite) + item + '\n' + + def emph(self, text): + return r'\emph{' + text.strip() + '}' + + def block(self, text): + return text.strip() + + def block_sep(self): + return '\n\\newblock ' + + # Stuff for rendering + + def render_str(self, string, which): + if isinstance(string, crosstex.parse.Value): + string = str(string.value) + elif 'short-' + which in self._flags: + string = str(string.shortname.value) + elif 'short-' + which not in self._flags: + string = str(string.longname.value) + return string + + def render_author(self, author, context=None, history=None): + author = [a.name.value if hasattr(a, 'name') else a.value for a in author] + if 'short-author' in self._flags: + author = crosstex.style.names_shortfirst_last(author) + else: + author = crosstex.style.names_first_last(author) + author = crosstex.style.list_comma_and(author) + return author + + def render_title(self, title, context=None, history=None): + title = title.value + if 'titlecase-default' in self._flags: + return title + elif 'titlecase-upper' in self._flags: + return crosstex.style.title_uppercase(title) + elif 'titlecase-title' in self._flags: + return crosstex.style.title_titlecase(title, self._titlephrases) + elif 'titlecase-lower' in self._flags: + return crosstex.style.title_lowercase(title, self._titlesmalls) + return title + + def render_booktitle(self, booktitle, context=None, history=None): + if isinstance(booktitle, crosstex.objects.workshop): + return self.render_str(booktitle, 'workshop') + elif isinstance(booktitle, crosstex.objects.conference): + return self.render_str(booktitle, 'conference') + elif isinstnace(booktitle, crosstex.parse.Value): + return self.render_str(booktitle, 'booktitle') + + def render_journal(self, journal, context=None, history=None): + return self.render_str(journal, 'journal') + + def render_pages(self, pages, context=None, history=None): + pages = str(pages) + if '-' in pages: + return 'pages %s' % pages + else: + return 'page %s' % pages + + def render_address(self, address, context=None, history=None): + city, state, country = None, None, None + if isinstance(address, crosstex.objects.location): + if address.city: + city = self.render_str(address.city, 'city') + if address.state: + state = self.render_str(address.state, 'state') + if address.country: + country = self.render_str(address.country, 'country') + elif isinstance(address, crosstex.objects.country): + country = self.render_str(address, 'country') + elif isinstance(address, crosstex.objects.state): + state = self.render_str(address, 'state') + if address.country: + country = self.render_str(address.country, 'country') + elif isinstance(address, crosstex.parse.Value): + return self.render_str(address, 'address') + return ', '.join([x for x in (city, state, country) if x is not None]) + + def render_year(self, year, context=None, history=None): + if isinstance(year, crosstex.parse.Value): + return self.render_str(year, 'year') + + def render_month(self, month, context=None, history=None): + return self.render_str(month, 'month') + + def render_article(self, article, context=None, history=None): + author = self.render_author(article.author) + title = self.render_title(article.title) + journal = self.render_journal(article.journal) + year = self.render_year(article.year) + volume = str(article.volume.value) if article.volume else None + number = str(article.number.value) if article.number else None + pages = str(article.pages.value) if article.pages else None + first = '' + second = '' + third = '' + if author: + first = self.block(crosstex.style.punctuate(author, '.', '')) + if title: + second = self.block(crosstex.style.punctuate(title, '.', '')) + if journal: + if 'add-in' in self._flags: + third += 'In ' + third += self.emph(journal) + volnumpages = '' + if number or volume or pages: + if volume: + volnumpages += str(volume) + if number: + volnumpages += '(%s)' % number + if pages: + if volume or number: + volnumpages += ':%s' % pages + else: + volnumpages += self.render_pages(pages) + if volnumpages: + third = crosstex.style.punctuate(third, ',', ' ') + third += volnumpages + if year: + third = crosstex.style.punctuate(third, ',', ' ') + year + third = crosstex.style.punctuate(third, '.', '') + third = self.block(third) + return self.block_sep().join([b for b in (first, second, third) if b]) + + def render_book(self, book, context=None, history=None): + author = self.render_author(book.author) + # XXX need to handle editors + title = self.render_title(book.title) + publisher = self.render_str(book.publisher, 'publisher') if book.publisher else None + address = self.render_address(book.address) if book.address else None + year = self.render_year(book.year) if book.year else None + first = '' + second = '' + third = '' + if author: + first = self.block(crosstex.style.punctuate(author, '.', '')) + if title: + second = self.block(crosstex.style.punctuate(title, '.', '')) + if publisher: + third = publisher + if address: + third = crosstex.style.punctuate(third, ',', ' ') + address + if year: + third = crosstex.style.punctuate(third, ',', ' ') + year + third = crosstex.style.punctuate(third, '.', '') + third = self.block(third) + return self.block_sep().join([b for b in (first, second, third) if b]) + + def render_inproceedings(self, inproceedings, context=None, history=None): + author = self.render_author(inproceedings.author) + title = self.render_title(inproceedings.title) + booktitle = self.render_booktitle(inproceedings.booktitle) + pages = self.render_pages(inproceedings.pages.value) if inproceedings.pages else None + address = self.render_address(inproceedings.address) if inproceedings.address else None + year = self.render_year(inproceedings.year) if inproceedings.year else None + month = self.render_month(inproceedings.month) if inproceedings.month else None + first = '' + second = '' + third = '' + if author: + first = self.block(crosstex.style.punctuate(author, '.', '')) + if title: + second = self.block(crosstex.style.punctuate(title, '.', '')) + if booktitle: + if 'add-in' in self._flags: + third += 'In ' + if 'add-proceedings' in self._flags: + third += 'Proceedings of the ' + elif 'add-proc' in self._flags: + third += 'Proc. of ' + third += crosstex.style.punctuate(self.emph(booktitle), ',', ' ') + if pages: + third = crosstex.style.punctuate(third, ',', ' ') + pages + if address: + third = crosstex.style.punctuate(third, ',', ' ') + address + if month and year: + third = crosstex.style.punctuate(third, ',', ' ') + third += month + ' ' + year + elif year: + third = crosstex.style.punctuate(third, ',', ' ') + third += year + third = crosstex.style.punctuate(third, '.', '') + third = self.block(third) + return self.block_sep().join([b for b in (first, second, third) if b]) + + def render_misc(self, misc, context=None, history=None): + author = self.render_author(misc.author) if misc.author else None + title = self.render_title(misc.title) if misc.title else None + howpub = str(misc.howpublished.value) if misc.howpublished else None + booktitle = self.render_booktitle(misc.booktitle) if misc.booktitle else None + address = self.render_address(misc.address) if misc.address else None + year = self.render_year(misc.year) if misc.year else None + first = '' + second = '' + third = '' + if author: + first = self.block(crosstex.style.punctuate(author, '.', '')) + if title: + second = self.block(crosstex.style.punctuate(title, '.', '')) + if howpub: + third += howpub + if booktitle: + third = crosstex.style.punctuate(third, ',', ' ') + self.emph(booktitle) + if address: + third = crosstex.style.punctuate(third, ',', ' ') + address + if year: + third = crosstex.style.punctuate(third, ',', ' ') + year + third = crosstex.style.punctuate(third, '.', '') + third = self.block(third) + return self.block_sep().join([b for b in (first, second, third) if b]) + + def render_techreport(self, techreport, context=None, history=None): + author = self.render_author(techreport.author) + title = self.render_title(techreport.title) + number = str(techreport.number.value) if techreport.number else None + insti = str(techreport.institution.value) if techreport.institution else None + address = self.render_address(techreport.address) if techreport.address else None + year = self.render_year(techreport.year) + month = self.render_month(techreport.month) if techreport.month else None + first = '' + second = '' + third = '' + if author: + first = self.block(crosstex.style.punctuate(author, '.', '')) + if title: + second = self.block(crosstex.style.punctuate(title, '.', '')) + if insti: + third = insti + if address: + third = crosstex.style.punctuate(third, ',', ' ') + address + if number: + third = crosstex.style.punctuate(third, ',', ' ') + third += 'Technical Report ' + number + if year: + third = crosstex.style.punctuate(third, ',', ' ') + year + third = crosstex.style.punctuate(third, '.', '') + third = self.block(third) + return self.block_sep().join([b for b in (first, second, third) if b]) + + def render_url(self, url, context=None, history=None): + author = self.render_author(url.author) if url.author else None + title = self.render_title(url.title) if url.title else None + link = str(url.url.value) + month = self.render_month(url.accessmonth) if url.accessmonth else None + day = self.render_str(url.accessday, 'day') if url.accessday else None + year = self.render_year(url.accessyear) if url.accessyear else None + first = '' + second = '' + third = '' + if author: + first = self.block(crosstex.style.punctuate(author, '.', '')) + if title: + second = self.block(crosstex.style.punctuate(title, '.', '')) + if url: + third = link + if month and day and year: + third = self.block(crosstex.style.punctuate(third, '.', '')) + third += 'Accessed ' + month + ' ' + day + ', ' + year + third = self.block(crosstex.style.punctuate(third, '.', '')) + third = self.block(third) + return self.block_sep().join([b for b in (first, second, third) if b]) diff --git a/crosstex/style/unsrt.py b/crosstex/style/unsrt.py deleted file mode 100644 index b7bd7ec..0000000 --- a/crosstex/style/unsrt.py +++ /dev/null @@ -1,14 +0,0 @@ -from crosstex.style.basic import * - -# Prefer long names. -string._addproducer(makegetterproducer('shortname'), 'value') -string._addproducer(makegetterproducer('longname'), 'value') - -# Use numeric labels. -publication._addproducer(emptyproducer, 'label') - -# Emphasize book and journal titles. -publication._addfilter(emphfilter, 'fullpublication', 'booktitle') -publication._addfilter(emphfilter, 'fullpublication', 'journal') -book._addfilter(emphfilter, 'fulltitle') -journal._addfilter(emphfilter, 'fulltitle') diff --git a/crosstex/style/xtx.py b/crosstex/style/xtx.py deleted file mode 100644 index 4eea14b..0000000 --- a/crosstex/style/xtx.py +++ /dev/null @@ -1,12 +0,0 @@ -from crosstex.style.common import * - -# Standard producers for top-level objects. -publication._addproducer(crosstexproducer, 'value') -string._addproducer(crosstexproducer, 'value') -location._addproducer(crosstexproducer, 'value') -ObjectList._addlistformatter(andcrosstexlistformatter, 'value') - -# Use empty labels (irrelevant, but labels determine what is 'citeable' and thus produced. -publication._addproducer(emptyproducer, 'label') -string._addproducer(emptyproducer, 'label') -location._addproducer(emptyproducer, 'label') diff --git a/scripts/dbfetch-post.pl b/scripts/dbfetch-post.pl deleted file mode 100755 index d1b6317..0000000 --- a/scripts/dbfetch-post.pl +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/perl -# -# Pull out @proceedings entries to the bottom and make them unique. -# -use strict; -use warnings; - -$/ = ''; -my @lines = <>; -my $i; - -$i = 1; -while ($i < @lines) { - if ($lines[$i] !~ /^\@proceedings/) { - print $lines[$i]; - splice @lines, $i, 1; - } else { - ++$i; - } -} - -exit; # Drop the @proceedings entries - -@lines = sort @lines; - -$i = 1; -while ($i < @lines) { - if ($lines[$i] eq $lines[$i - 1]) { - splice @lines, $i, 1; - } else { - ++$i; - } -} - -print @lines; diff --git a/scripts/dbfetch-post.sh b/scripts/dbfetch-post.sh deleted file mode 100755 index 5935f69..0000000 --- a/scripts/dbfetch-post.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -for name in "$@"; do - sed -e '/^ *}/a \\' $name.bib | ./dbfetch-post.pl > $name.xtx -done diff --git a/scripts/dbfetch.py b/scripts/dbfetch.py deleted file mode 100755 index 1cbe550..0000000 --- a/scripts/dbfetch.py +++ /dev/null @@ -1,49 +0,0 @@ -#! /usr/bin/env python -# -# Fetch content from DBLP -# -# Author: Emin Gun Sirer -# Updated by Robert Burgess January 2007 - -import thread -import socket -import time -import urllib -import re -import sys - - -base = 'http://www.informatik.uni-trier.de/~ley/db/conf/' -contentre = "Contents" -bibtexre = "BibTeX" - - -def getbibtex(str): - url = urllib.urlopen(str) - contents = url.read() - url.close() - match = re.compile("
(.*)
", re.S).search(contents) - bibtex = match.group(1) - return re.sub(r'<([^>]*)>', '', bibtex) - -for name in sys.argv[1:]: - file = open(name + '.bib', 'w') - - print name, base + name + '/index.html' - confurl = urllib.urlopen(base + name + '/index.html') - confcontents = confurl.read() - confurl.close() - - linkre = re.compile("") - - for confmatch in linkre.finditer(confcontents): - year = confmatch.group(1) - - print ' ' + year - yearurl = urllib.urlopen(base + name + '/' + year) - contents = yearurl.read() - yearurl.close() - - for match in re.compile(bibtexre, re.M).finditer(contents): - str = match.group(1) - file.write(getbibtex(str)) diff --git a/scripts/journalfilter.pl b/scripts/journalfilter.pl deleted file mode 100755 index 1bd14bd..0000000 --- a/scripts/journalfilter.pl +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/perl - -sub trim -{ - local $_ = shift; - s/^\s+//; - s/\s+$//; - return $_ -} - -while (<>) -{ - next if /^\s*(#.*)?$/; - - my ($long, $short) = split /=/; - my ($abbr, $comment) = split /;/, $short; - - $long = trim $long; - $abbr = trim $abbr; - - my $key = $abbr; - $key =~ s/\s+/_/g; - $key =~ s/\W//g; - $key = lc $key; - - print "\@journal{$key,\n\tname=\"$long\",\n"; - print "\tshortname=\"$abbr\",\n" if $abbr ne $long; - print "}\n\n"; -} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2704d3f --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from distutils.core import setup + + +classifiers = [ 'Development Status :: 5 - Production/Stable' + , 'Intended Audience :: Science/Research' + , 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' + , 'Operating System :: MacOS :: MacOS X' + , 'Operating System :: POSIX :: Linux' + , 'Operating System :: Unix' + ] + +setup(name='CrossTex', + version='0.8.dev', + author='Robert Escriva (maintainer)', + author_email='escriva@cs.cornell.edu', + packages=['crosstex' + ,'crosstex.style' + ], + scripts=['bin/crosstex'], + url='http://firmant.org/', + license='GPLv2', + description='CrossTeX is a bibliography management tool', + classifiers=classifiers, + requires=['ply', 'argparse', 'importlib'] + )