Completed
Push — master ( 018e13...a3c8d4 )
by Christophe
26s
created

numberingPara()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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