Completed
Push — master ( e6b3b0...c018d7 )
by Christophe
25s
created

applyJSONFilters()   C

Complexity

Conditions 7

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
c 1
b 0
f 0
dl 0
loc 45
rs 5.5
1
#!/usr/bin/env python
2
3
"""
4
Pandoc filter to number all kinds of things.
5
"""
6
7
from pandocfilters import walk, stringify, Str, Space, Para, BulletList, Plain, Strong, Span, Link, Emph, RawInline, RawBlock, Header, DefinitionList
8
from functools import reduce
9
import json
10
import io
11
import sys
12
import codecs
13
import re
14
import unicodedata
15
import subprocess
16
17
count = {}
18
information = {}
19
collections = {}
20
headers = [0, 0, 0, 0, 0, 0]
21
headerRegex = '(?P<header>(?P<hidden>(-\.)*)(\+\.)*)'
22
23
def toJSONFilters(actions):
24
    """Generate a JSON-to-JSON filter from stdin to stdout
25
26
    The filter:
27
28
    * reads a JSON-formatted pandoc document from stdin
29
    * transforms it by walking the tree and performing the actions
30
    * returns a new JSON-formatted pandoc document to stdout
31
32
    The argument `actions` is a list of functions of the form
33
    `action(key, value, format, meta)`, as described in more
34
    detail under `walk`.
35
36
    This function calls `applyJSONFilters`, with the `format`
37
    argument provided by the first command-line argument,
38
    if present.  (Pandoc sets this by default when calling
39
    filters.)
40
    """
41
    try:
42
        input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
43
    except AttributeError:
44
        # Python 2 does not have sys.stdin.buffer.
45
        # REF: https://stackoverflow.com/questions/2467928/python-unicodeencode
46
        input_stream = codecs.getreader("utf-8")(sys.stdin)
47
48
    source = input_stream.read()
49
    if len(sys.argv) > 1:
50
        format = sys.argv[1]
51
    else:
52
        format = ""
53
54
    sys.stdout.write(applyJSONFilters(actions, source, format))
55
56
def applyJSONFilters(actions, source, format=""):
57
    """Walk through JSON structure and apply filters
58
59
    This:
60
61
    * reads a JSON-formatted pandoc document from a source string
62
    * transforms it by walking the tree and performing the actions
63
    * returns a new JSON-formatted pandoc document as a string
64
65
    The `actions` argument is a list of functions (see `walk`
66
    for a full description).
67
68
    The argument `source` is a string encoded JSON object.
69
70
    The argument `format` is a string describing the output format.
71
72
    Returns a the new JSON-formatted pandoc document.
73
    """
74
75
    doc = json.loads(source)
76
77
    if 'pandoc-api-version' in doc:
78
        pandocVersion.value = doc['pandoc-api-version']
79
80
    if 'meta' in doc:
81
        meta = doc['meta']
82
    elif doc[0]:  # old API
83
        meta = doc[0]['unMeta']
84
    else:
85
        meta = {}
86
87
    altered = doc
88
    for action in actions:
89
        altered = walk(altered, action, format, meta)
90
91
    if 'meta' in altered:
92
        meta = altered['meta']
93
    elif meta[0]:  # old API
94
        meta = altered[0]['unMeta']
95
    else:
96
        meta = {}
97
98
    addListings(altered, format, meta)
99
100
    return json.dumps(altered)
101
102
def removeAccents(string):
103
    nfkd_form = unicodedata.normalize('NFKD', string)
104
    return u"".join([c for c in nfkd_form if not unicodedata.combining(c)])
105
106
def toIdentifier(string):
107
    # replace invalid characters by dash
108
    string = re.sub('[^0-9a-zA-Z_-]+', '-', removeAccents(string.lower()))
109
110
    # Remove leading digits
111
    string = re.sub('^[^a-zA-Z]+', '', string)
112
113
    return string
114
115
def toLatex(x):
116
    """Walks the tree x and returns concatenated string content,
117
    leaving out all formatting.
118
    """
119
    result = []
120
121
    def go(key, val, format, meta):
122
        if key in ['Str', 'MetaString']:
123
            result.append(val)
124
        elif key == 'Code':
125
            result.append(val[1])
126
        elif key == 'Math':
127
            # Modified from the stringify function in the pandocfilter package
128
            if format == 'latex':
129
                result.append('$' + val[1] + '$')
130
            else:
131
                result.append(val[1])
132
        elif key == 'LineBreak':
133
            result.append(" ")
134
        elif key == 'Space':
135
            result.append(" ")
136
        elif key == 'Note':
137
            # Do not stringify value from Note node
138
            del val[:]
139
140
    walk(x, go, 'latex', {})
141
    return ''.join(result)
142
143
def numbering(key, value, format, meta):
144
    if key == 'Header':
145
        return numberingHeader(value)
146
    elif key == 'Para':
147
        return numberingPara(value, format, meta)
148
    elif key == 'DefinitionList':
149
        return numberingDefinitionList(value, format, meta)
150
151
def numberingHeader(value):
152
    [level, [id, classes, attributes], content] = value
153
    if 'unnumbered' not in classes:
154
        headers[level - 1] = headers[level - 1] + 1
155
        for index in range(level, 6):
156
            headers[index] = 0
157
158
def numberingInlines(value, format, meta):
159
    if len(value) >= 3 and value[-2]['t'] == 'Space' and value[-1]['t'] == 'Str':
160
        last = value[-1]['c']
161
        match = re.match('^' + headerRegex + '#((?P<prefix>[a-zA-Z][\w.-]*):)?(?P<name>[a-zA-Z][\w:.-]*)?$', last)
162
        if match:
163
            # Is the last element an identifier beginning with '#'
164
            return numberingEffective(match, value, format, meta)
165
        elif re.match('^' + headerRegex + '##(?P<prefix>[a-zA-Z][\w.-]*:)?(?P<name>[a-zA-Z][\w:.-]*)?$', last):
166
            # Special case where the last element is '...##...'
167
            return numberingSharpSharp(value)
168
    return value
169
170
def numberingPara(value, format, meta):
171
    return Para(numberingInlines(value, format, meta))
172
173
def numberingDefinitionList(value, format, meta):
174
    return DefinitionList([
175
        [numberingInlines(term, format, meta), defs]
176
        for [term, defs] in value
177
    ])
178
179
def numberingEffective(match, value, format, meta):
180
    title = computeTitle(value)
181
    description = computeDescription(value)
182
    basicCategory = computeBasicCategory(match, description)
183
    [levelInf, levelSup] = computeLevels(match, basicCategory, meta)
184
    sectionNumber = computeSectionNumber(levelSup)
185
    leading = computeLeading(levelSup, sectionNumber)
186
    category = computeCategory(basicCategory, leading)
187
    number = str(count[category])
188
    tag = computeTag(match, basicCategory, category, number)
189
    localNumber = computeLocalNumber(levelInf, levelSup, number)
190
    globalNumber = computeGlobalNumber(sectionNumber, number)
191
    [text, link, toc] = computeTextLinkToc(meta, basicCategory, description, title, localNumber, globalNumber, sectionNumber)
192
193
    # Store the numbers and the label for automatic numbering (See referencing function)
194
    information[tag] = {
195
        'section': sectionNumber,
196
        'local': localNumber,
197
        'global': globalNumber,
198
        'count': number,
199
        'description': description,
200
        'title': title,
201
        'link': link,
202
        'toc': toc
203
    }
204
205
    # Prepare the contents
206
    contents = [Span([tag, ['pandoc-numbering-text'] + getClasses(basicCategory, meta), []], text)]
207
208
    # Compute collections
209
    if basicCategory not in collections:
210
        collections[basicCategory] = []
211
212
    collections[basicCategory].append(tag)
213
214
    # Special case for LaTeX
215
    if format == 'latex' and getFormat(basicCategory, meta):
216
        addLaTeX(contents, basicCategory, title, description, leading, number)
217
218
    return contents
219
220
def computeTitle(value):
221
    title = []
222
    if value[-3]['t'] == 'Str' and value[-3]['c'][-1:] == ')':
223
        for (i, item) in enumerate(value):
224
            if item['t'] == 'Str' and item['c'][0] == '(':
225
                title = value[i:-2]
226
                title[0]['c'] = title[0]['c'][1:]
227
                title[-1]['c'] = title[-1]['c'][:-1]
228
                del value[i-1:-2]
229
                break
230
    return title
231
232
def computeDescription(value):
233
    return value[:-2]
234
235
def computeBasicCategory(match, description):
236
    if match.group('prefix') == None:
237
        return toIdentifier(stringify(description))
238
    else:
239
        return match.group('prefix')
240
241
def computeLevels(match, basicCategory, meta):
242
    # Compute the levelInf and levelSup values
243
    levelInf = len(match.group('hidden')) // 2
244
    levelSup = len(match.group('header')) // 2
245
246
    # Get the default inf and sup level
247
    if levelInf == 0 and levelSup == 0:
248
        [levelInf, levelSup] = getDefaultLevels(basicCategory, meta)
249
250
    return [levelInf, levelSup]
251
252
def computeSectionNumber(levelSup):
253
    return '.'.join(map(str, headers[:levelSup]))
254
255
def computeLeading(levelSup, sectionNumber):
256
    # Compute the leading (composed of the section numbering and a dot)
257
    if levelSup != 0:
258
        return sectionNumber + '.'
259
    else:
260
        return ''
261
262
def computeCategory(basicCategory, leading):
263
    category = basicCategory + ':' + leading
264
265
    # Is it a new category?
266
    if category not in count:
267
        count[category] = 0
268
269
    count[category] = count[category] + 1
270
271
    return category
272
273
def computeTag(match, basicCategory, category, number):
274
    # Determine the final tag
275
    if match.group('name') == None:
276
        return category + number
277
    else:
278
        return basicCategory + ':' + match.group('name')
279
280
def computeLocalNumber(levelInf, levelSup, number):
281
    # Replace the '-.-.+.+...#' by the category count (omitting the hidden part)
282
    return '.'.join(map(str, headers[levelInf:levelSup] + [number]))
283
284
def computeGlobalNumber(sectionNumber, number):
285
    # Compute the globalNumber
286
    if sectionNumber:
287
        return sectionNumber + '.' + number
288
    else:
289
        return number
290
291
def computeTextLinkToc(meta, basicCategory, description, title, localNumber, globalNumber, sectionNumber):
292
    # Is the automatic formatting required for this category?
293
    if getFormat(basicCategory, meta):
294
        # Prepare the final text
295
        text = [Strong(description + [Space(), Str(localNumber)])]
296
297
        # Add the title to the final text
298
        if title:
299
            text = text + [Space(), Emph([Str('(')] + title + [Str(')')])]
300
301
        # Compute the link
302
        link = description + [Space(), Str(localNumber)]
303
304
        # Compute the toc
305
        toc = [Str(globalNumber), Space()]
306
        if title:
307
            toc = toc + title
308
        else:
309
            toc = toc + description
310
311
    else:
312
        # Prepare the final text
313
        text = [
314
            Span(['', ['description'], []], description),
315
            Span(['', ['title'], []], title),
316
            Span(['', ['local'], []], [Str(localNumber)]),
317
            Span(['', ['global'], []], [Str(globalNumber)]),
318
            Span(['', ['section'], []], [Str(sectionNumber)]),
319
        ]
320
321
        # Compute the link
322
        link = [Span(['', ['pandoc-numbering-link'] + getClasses(basicCategory, meta), []], text)]
323
324
        # Compute the toc
325
        toc = [Span(['', ['pandoc-numbering-toc'] + getClasses(basicCategory, meta), []], text)]
326
    return [text, link, toc]
327
328
def addLaTeX(contents, basicCategory, title, description, leading, number):
329
    latexCategory = re.sub('[^a-z]+', '', basicCategory)
330
    if title:
331
      entry = title
332
    else:
333
      entry = description
334
    latex = '\\phantomsection\\addcontentsline{' + latexCategory + '}{' + latexCategory + '}{\\protect\\numberline {' + \
335
        leading + number + '}{\ignorespaces ' + toLatex(entry) + '}}'
336
    contents.insert(0, RawInline('tex', latex))
337
338
def numberingSharpSharp(value):
339
    value[-1]['c'] = value[-1]['c'].replace('##', '#', 1)
340
    return value
341
342
replace = None
343
search = None
344
345
def lowering(key, value, format, meta):
346
    if key == 'Str':
347
        return Str(value.lower())
348
349
def referencing(key, value, format, meta):
350
    if key == 'Link':
351
        return referencingLink(value, format, meta)
352
    elif key == 'Cite':
353
        return referencingCite(value, format, meta)
354
355
def referencingLink(value, format, meta):
356
    global replace, search
357
    if pandocVersion() < '1.16':
358
        # pandoc 1.15
359
        [text, [reference, title]] = value
360
    else:
361
        # pandoc > 1.15
362
        [attributes, text, [reference, title]] = value
363
364
    if re.match('^(#([a-zA-Z][\w:.-]*))$', reference):
365
        # Compute the name
366
        tag = reference[1:]
367
368
        if tag in information:
369
            if pandocVersion() < '1.16':
370
                # pandoc 1.15
371
                i = 0
372
            else:
373
                # pandoc > 1.15
374
                i = 1
375
376
            # Replace all '#t', '#T', '#d', '#D', '#s', '#g', '#c', '#n', '#' with the corresponding text in the title
377
            value[i + 1][1] = value[i + 1][1].replace('#t', stringify(information[tag]['title']).lower())
378
            value[i + 1][1] = value[i + 1][1].replace('#T', stringify(information[tag]['title']))
379
            value[i + 1][1] = value[i + 1][1].replace('#d', stringify(information[tag]['description']).lower())
380
            value[i + 1][1] = value[i + 1][1].replace('#D', stringify(information[tag]['description']))
381
            value[i + 1][1] = value[i + 1][1].replace('#s', information[tag]['section'])
382
            value[i + 1][1] = value[i + 1][1].replace('#g', information[tag]['global'])
383
            value[i + 1][1] = value[i + 1][1].replace('#c', information[tag]['count'])
384
            value[i + 1][1] = value[i + 1][1].replace('#n', information[tag]['local'])
385
            value[i + 1][1] = value[i + 1][1].replace('#', information[tag]['local'])
386
387
            if text == []:
388
                # The link text is empty, replace it with the default label
389
                value[i] = information[tag]['link']
390
            else:
391
                # The link text is not empty
392
393
                #replace all '#t' with the title in lower case
394
                replace = walk(information[tag]['title'], lowering, format, meta)
395
                search = '#t'
396
                value[i] = walk(value[i], replacing, format, meta)
397
398
                #replace all '#T' with the title
399
                replace = information[tag]['title']
400
                search = '#T'
401
                value[i] = walk(value[i], replacing, format, meta)
402
403
                #replace all '#d' with the description in lower case
404
                replace = walk(information[tag]['description'], lowering, format, meta)
405
                search = '#d'
406
                value[i] = walk(value[i], replacing, format, meta)
407
408
                #replace all '#D' with the description
409
                replace = information[tag]['description']
410
                search = '#D'
411
                value[i] = walk(value[i], replacing, format, meta)
412
413
                #replace all '#s' with the corresponding number
414
                replace = [Str(information[tag]['section'])]
415
                search = '#s'
416
                value[i] = walk(value[i], replacing, format, meta)
417
418
                #replace all '#g' with the corresponding number
419
                replace = [Str(information[tag]['global'])]
420
                search = '#g'
421
                value[i] = walk(value[i], replacing, format, meta)
422
423
                #replace all '#c' with the corresponding number
424
                replace = [Str(information[tag]['count'])]
425
                search = '#c'
426
                value[i] = walk(value[i], replacing, format, meta)
427
428
                #replace all '#n' with the corresponding number
429
                replace = [Str(information[tag]['local'])]
430
                search = '#n'
431
                value[i] = walk(value[i], replacing, format, meta)
432
433
                #replace all '#' with the corresponding number
434
                replace = [Str(information[tag]['local'])]
435
                search = '#'
436
                value[i] = walk(value[i], replacing, format, meta)
437
438
def referencingCite(value, format, meta):
439
    match = re.match('^(@(?P<tag>(?P<category>[a-zA-Z][\w.-]*):(([a-zA-Z][\w.-]*)|(\d*(\.\d*)*))))$', value[1][0]['c'])
440
    if match != None and getCiteShortCut(match.group('category'), meta):
441
442
        # Deal with @prefix:name shortcut
443
        tag = match.group('tag')
444
        if tag in information:
445
            if pandocVersion() < '1.16':
446
                # pandoc 1.15
447
                return Link([Str(information[tag]['local'])], ['#' + tag, ''])
448
            else:
449
                # pandoc > 1.15
450
                return Link(['', [], []], [Str(information[tag]['local'])], ['#' + tag, ''])
451
452
def replacing(key, value, format, meta):
453
    if key == 'Str':
454
        prepare = value.split(search)
455
        if len(prepare) > 1:
456
457
            ret = []
458
459
            if prepare[0] != '':
460
                ret.append(Str(prepare[0]))
461
462
            for string in prepare[1:]:
463
                ret.extend(replace)
464
                if string != '':
465
                    ret.append(Str(string))
466
467
            return ret
468
469
def hasMeta(meta):
470
    return 'pandoc-numbering' in meta and meta['pandoc-numbering']['t'] == 'MetaList'
471
472
def isCorrect(definition):
473
    return definition['t'] == 'MetaMap' and\
474
        'category' in definition['c'] and\
475
        definition['c']['category']['t'] == 'MetaInlines' and\
476
        len(definition['c']['category']['c']) == 1 and\
477
        definition['c']['category']['c'][0]['t'] == 'Str'
478
479
def hasProperty(definition, name, type):
480
    return name in definition['c'] and definition['c'][name]['t'] == type
481
482
def getProperty(definition, name):
483
    return definition['c'][name]['c']
484
485
def getFirstValue(definition, name):
486
	return getProperty(definition, name)[0]['c']
487
488
def addListings(doc, format, meta):
489
    if hasMeta(meta):
490
        listings = []
491
492
        # Loop on all listings definition
493
        for definition in meta['pandoc-numbering']['c']:
494
            if isCorrect(definition) and hasProperty(definition, 'listing', 'MetaInlines'):
495
496
                # Get the category name
497
                category = getFirstValue(definition, 'category')
498
499
                # Get the title
500
                title = getProperty(definition, 'listing')
501
502
                listings.append(Header(1, ['', ['unnumbered'], []], title))
503
504
                if format == 'latex':
505
                    extendListingsLaTeX(listings, meta, definition, category)
506
                else:
507
                    extendListingsOther(listings, meta, definition, category)
508
509
        # Add listings to the document
510
        if 'blocks' in doc:
511
            doc['blocks'][0:0] = listings
512
        else:  # old API
513
            doc[1][0:0] = listings
514
515
def extendListingsLaTeX(listings, meta, definition, category):
516
    space = getSpace(definition, category)
517
    tab = getTab(definition, category)
518
    # Add a RawBlock
519
    latexCategory = re.sub('[^a-z]+', '', category)
520
    latex = [
521
        getLinkColor(meta),
522
        '\\makeatletter',
523
        '\\newcommand*\\l@' + latexCategory + '{\\@dottedtocline{1}{' + str(tab) + 'em}{'+ str(space) +'em}}',
524
        '\\@starttoc{' + latexCategory + '}',
525
        '\\makeatother'
526
    ]
527
    listings.append(RawBlock('tex', ''.join(latex)))
528
529
def getLinkColor(meta):
530
    # Get the link color
531
    if 'toccolor' in meta:
532
        return '\\hypersetup{linkcolor=' + stringify(meta['toccolor']['c']) + '}'
533
    else:
534
        return '\\hypersetup{linkcolor=black}'
535
536
def getTab(definition, category):
537
    # Get the tab
538
    if hasProperty(definition, 'tab', 'MetaString'):
539
        try:
540
            tab = float(getProperty(definition, 'tab'))
541
        except ValueError:
542
            tab = None
543
    else:
544
        tab = None
545
546
    # Deal with default tab length
547
    if tab == None:
548
        return 1.5
549
    else:
550
        return tab
551
552
def getSpace(definition, category):
553
    # Get the space
554
    if hasProperty(definition, 'space', 'MetaString'):
555
        try:
556
            space = float(getProperty(definition, 'space'))
557
        except ValueError:
558
            space = None
559
    else:
560
        space = None
561
562
    # Deal with default space length
563
    if space == None:
564
        level = 0
565
        if category in collections:
566
            # Loop on the collection
567
            for tag in collections[category]:
568
                level = max(level, information[tag]['section'].count('.'))
569
        return level + 2.3
570
    else:
571
        return space
572
573
def extendListingsOther(listings, meta, definition, category):
574
    if category in collections:
575
        # Prepare the list
576
        elements = []
577
578
        # Loop on the collection
579
        for tag in collections[category]:
580
581
            # Add an item to the list
582
            text = information[tag]['toc']
583
584
            if pandocVersion() < '1.16':
585
                # pandoc 1.15
586
                link = Link(text, ['#' + tag, ''])
587
            else:
588
                # pandoc 1.16
589
                link = Link(['', [], []], text, ['#' + tag, ''])
590
591
            elements.append([Plain([link])])
592
593
        # Add a bullet list
594
        listings.append(BulletList(elements))
595
596
def getValue(category, meta, fct, default, analyzeDefinition):
597
    if not hasattr(fct, 'value'):
598
        fct.value = {}
599
        if hasMeta(meta):
600
            # Loop on all listings definition
601
            for definition in meta['pandoc-numbering']['c']:
602
                if isCorrect(definition):
603
                    analyzeDefinition(definition)
604
605
    if not category in fct.value:
606
        fct.value[category] = default
607
608
    return fct.value[category]
609
610
def getFormat(category, meta):
611
    def analyzeDefinition(definition):
612
        if hasProperty(definition, 'format', 'MetaBool'):
613
            getFormat.value[getFirstValue(definition, 'category')] = getProperty(definition, 'format')
614
        
615
    return getValue(category, meta, getFormat, True, analyzeDefinition)
616
617
def getCiteShortCut(category, meta):
618
    def analyzeDefinition(definition):
619
        if hasProperty(definition, 'cite-shortcut', 'MetaBool'):
620
            getCiteShortCut.value[getFirstValue(definition, 'category')] = getProperty(definition, 'cite-shortcut')
621
622
    return getValue(category, meta, getCiteShortCut, False, analyzeDefinition)
623
624
def getLevelsFromYaml(definition):
625
    levelInf = 0
626
    levelSup = 0
627
    if hasProperty(definition, 'first', 'MetaString'):
628
        try:
629
            levelInf = max(min(int(getProperty(definition, 'first')) - 1, 6), 0)
630
        except ValueError:
631
            pass
632
    if hasProperty(definition, 'last', 'MetaString'):
633
        try:
634
            levelSup = max(min(int(getProperty(definition, 'last')), 6), levelInf)
635
        except ValueError:
636
            pass
637
    return [levelInf, levelSup]
638
639
def getLevelsFromRegex(definition):
640
    match = re.match('^' + headerRegex + '$', getFirstValue(definition, 'sectioning'))
641
    if match:
642
        # Compute the levelInf and levelSup values
643
        return [len(match.group('hidden')) // 2, len(match.group('header')) // 2]
644
    else:
645
        return [0, 0]
646
647
def getDefaultLevels(category, meta):
648
    def analyzeDefinition(definition):
649
        if hasProperty(definition, 'sectioning', 'MetaInlines') and\
650
           len(getProperty(definition, 'sectioning')) == 1 and\
651
           getProperty(definition, 'sectioning')[0]['t'] == 'Str':
652
653
            getDefaultLevels.value[getFirstValue(definition, 'category')] = getLevelsFromRegex(definition)
654
        else:
655
            getDefaultLevels.value[getFirstValue(definition, 'category')] = getLevelsFromYaml(definition)
656
657
    return getValue(category, meta, getDefaultLevels, [0, 0], analyzeDefinition)
658
659
def getClasses(category, meta): 
660
    def analyzeDefinition(definition):
661
        if hasProperty(definition, 'classes', 'MetaList'):
662
            classes = []
663
            for elt in getProperty(definition, 'classes'):
664
                classes.append(stringify(elt))
665
            getClasses.value[getFirstValue(definition, 'category')] = classes
666
667
    return getValue(category, meta, getClasses, [category], analyzeDefinition)
668
669
def pandocVersion():
670
    if not hasattr(pandocVersion, 'value'):
671
        p = subprocess.Popen(['pandoc', '-v'], stdout=subprocess.PIPE,stderr=subprocess.PIPE)
672
        out, err = p.communicate()
673
        pandocVersion.value = re.search(b'pandoc (?P<version>.*)', out).group('version').decode('utf-8')
674
    return pandocVersion.value
675
676
def main():
677
    toJSONFilters([numbering, referencing])
678
679
if __name__ == '__main__':
680
    main()
681