source: tol/tolbaseBLT/lib/markupparser/markupparser.tcl

devel
Last change on this file was 56ade3b, checked in by Jorge Pérez <josp.jorge@…>, 10 years ago

fixes #1932, se ha creado la rama trunk/tolbaseBLT copia de trunk/tolbase

git-svn-id: ​https://www.tol-project.org/svn/tolp/trunk@7330 be4846ae-f051-483a-831f-b64e7d08527f

  • Property mode set to 100644
File size: 19.8 KB
Line 
1#-----------------------------------------------------------------------
2# TITLE:
3#       markupparser.tcl
4#
5# AUTHOR:
6#       Will Duquette
7#
8# DESCRIPTION:
9#       Notebook application's markup language parser.
10#
11#       Speed-up ideas:
12#       - Search for next boundary using normal string search.
13#       - Use a hidden text widget to search for each boundary using
14#         regexps.
15#
16# LICENSE:
17#       Copyright (C) 2005 by William H. Duquette.  This file may
18#       be used subject to the terms in license.txt.
19#
20#-----------------------------------------------------------------------
21
22#-----------------------------------------------------------------------
23# Required Packages
24
25package require textutil
26
27#-----------------------------------------------------------------------
28# Provided package
29
30package provide markupparser 2.0
31
32namespace eval ::markupparser:: {
33    namespace export {[a-z]*}
34}
35
36
37#-----------------------------------------------------------------------
38# Public Functions
39
40# Parses text, generating a list directly
41proc ::markupparser::parse {text} {
42    set result {}
43
44    # Q: Would scanning the string without modification be quicker?
45    while {[string length $text] > 0} {
46        # First, blank lines go as "NL".
47        if {[regexp {^(\s*\n)(.*)$} $text dummy para text]} {
48            lappend result NL $para
49            continue
50        }
51
52        # NEXT, handle headers.  Header lines begin and end with
53        # 1, 2, or 3 "=" signs, separated from the header text by
54        # at least one space.  Headers are passed as H tags.
55        if {[string index $text 0] eq "="} {
56            # Header level 1
57            if {[regexp {^= (\S[^\n]*\S*) =\n(.*)$} $text dum val text]} {
58                lappend result H [list 1 $val]
59                continue
60            }
61
62            # Header level 2
63            if {[regexp {^== (\S[^\n]*\S*) ==\n(.*)$} $text dum val text]} {
64                lappend result H [list 2 $val]
65                continue
66            }
67
68            # Header level 3
69            if {[regexp {^=== (\S[^\n]*\S*) ===\n(.*)$} $text dum val text]} {
70                lappend result H [list 3 $val]
71                continue
72            }
73        }
74
75        # NEXT, handle directives.  All directives are passed along
76        # as HASH.  #pre and #Tcl are handled specially.
77        if {[string index $text 0] eq "#"} {
78            if {![regexp {^(\#[^\n]*\n)(.*)$} $text dummy para text]} {
79                set para $text
80                set text ""
81            }
82            lappend result HASH $para
83
84            # If the directive is #pre, then we have preformatted text
85            # until \n#unpre or the end of the text.
86            if {[string match "#pre*" $para]} {
87                # First, find the end.
88                set ndx [string first "\n\#unpre" $text]
89
90                if {$ndx == -1} {
91                    lappend result PRE $text
92                    set text ""
93                } else {
94                    lappend result PRE [string range $text 0 $ndx]
95                    incr ndx
96                    set text [string range $text $ndx end]
97                }
98                continue
99            }
100               
101            # If the directive is #Tcl, then we have preformatted text
102            # until \n#unTcl or the end of the text.
103            if {[string match "#Tcl*" $para]} {
104                # First, find the end.
105                set ndx [string first "\n\#unTcl" $text]
106
107                if {$ndx == -1} {
108                    lappend result TCL $text
109                    set text ""
110                } else {
111                    lappend result TCL [string range $text 0 $ndx]
112                    incr ndx
113                    set text [string range $text $ndx end]
114                }
115                continue
116            }
117
118            # If the directive is #data then we have to retrieve the
119            # data name from the line, and then the text up to
120            # \n#undata or the end of the text.
121            if {[string match "#data*" $para]} {
122                # FIRST, prepare the fields
123                array set fields {
124                    type {}
125                    name {}
126                    options {}
127                    data {}
128                }
129                # FIRST, get the data arguments, if any.
130                set parms ""
131                if {[regexp {^\#data\s+(.*)$} $para dummy parms]} {
132                    if {[catch {lindex $parms 0} dummy]} {
133                        set parms [split [normalizeSpace $parms] " "]
134                    }
135
136                    set fields(type) [lindex $parms 0]
137                    set fields(name) [lindex $parms 1]
138                    set fields(options) [lrange $parms 2 end]
139                }
140
141                # NEXT, find the end.
142                set ndx [string first "\n\#undata" $text]
143
144                if {$ndx == -1} {
145                    set fields(data) $text
146                    set text ""
147                } else {
148                    set fields(data) [string range $text 0 $ndx]
149                    incr ndx
150                    set text [string range $text $ndx end]
151                }
152
153                lappend result DATA [array get fields]
154                continue
155            }
156
157            # If the directive is #meta then we have to retrieve the value.
158            if {[regexp {^\#meta\s+(\S+)(\s+.*)?$} $para dummy name value]} {
159                set meta($name) [string trim $value]
160                continue
161            }
162
163            continue
164        }
165
166        # At this point, we know the following things:
167        # The next line is not blank, with or without extra whitespace.
168        # The next line doesn't begin with a #.
169        #
170        # Given that, find the end of the paragraph.
171       
172        set eop [FindEndOfPara $text]
173
174        if {$eop == -1} {
175            set para $text
176            set text ""
177        } else {
178            set para [string range $text 0 $eop]
179            set text [string range $text [expr {$eop + 1}] end]
180        }
181
182        # Next, if it begins with whitespace it's preformatted.
183        set firstChar [string index $para 0]
184
185        if {" " == $firstChar || "\t" == $firstChar} {
186            lappend result PRE $para
187            continue
188        }
189
190        # NEXT, if it begins with "*" or ":" it's bulleted or indented;
191        # if it begins with "-" it's normal but the "-" + leading is
192        # not displayed.  Otherwise it's just normal.
193       
194        if {"*" == $firstChar} {
195            regexp {^(\*+)(\s*)(.*$)} $para dummy stars leading para
196            lappend result P [list * [string length $stars] $leading]
197        } elseif {":" == $firstChar} {
198            regexp {^(:+)(\s*)(.*$)} $para dummy colons leading para
199            lappend result P [list : [string length $colons] $leading]
200        } elseif {"-" == $firstChar} {
201            regexp {^-(\s*)(.*$)} $para dummy leading para
202            lappend result P [list : 0 "-$leading"]
203        } else {
204            lappend result P [list : 0 ""]
205        }
206
207        # NEXT, keep track of the text style flags.  All are off
208        # at the beginning of each paragraph.
209        foreach letter [list b i m h s x] {
210            set styflag($letter) 0
211        }
212
213        # Next, parse out the elements of the paragraph:
214        while {[string length $para] > 0} {
215
216            # FIRST, handle wiki-like style markup.
217            if {[string index $para 0] == "'"} {
218                # Toggle Bold face
219                if {[regexp {^(''')(.*$)} $para dummy tag para]} {
220                    set styflag(b) [expr {!$styflag(b)}]
221                    lappend result STY [list b$styflag(b) $tag]
222                    continue
223                }
224             
225                # Toggle Italics
226                if {[regexp {^('')(.*$)} $para dummy tag para]} {
227                    set styflag(i) [expr {!$styflag(i)}]
228                    lappend result STY [list i$styflag(i) $tag]
229                    continue
230                }
231            }
232
233            # NEXT, handle HTML-like style markup.
234            if {[string index $para 0] == "<"} {
235                if {[regexp {^(</?([bimhsx])>)(.*$)} \
236                         $para dummy tag letter para]} {
237
238                    if {[string index $tag 1] eq "/"} {
239                        set flag 0
240                    } else {
241                        set flag 1
242                    }
243                    set styflag($letter) $flag
244                    lappend result STY [list $letter$flag $tag]
245                    continue
246                }
247
248                if {[regexp {^(<br>\s*)(.*$)} $para dummy tag para]} {
249                    lappend result BR $tag
250                    continue
251                }
252
253                if {[regexp {^<html>(.*$)} $para dummy para]} {
254                    # find the end.
255                    set ndx [string first "</html>" $para]
256
257                    if {$ndx == -1} {
258                        lappend result HTML $para
259                        set para ""
260                    } else {
261                        incr ndx -1
262                        lappend result HTML [string range $para 0 $ndx]
263                        incr ndx 8; # Skip the </html>
264                        set para [string range $para $ndx end]
265                    }
266                    continue
267                }
268            }
269
270
271            # NEXT, handle buttons, leftover macros, objects, and links.
272            if {[string index $para 0] == "\["} {
273                if {[string index $para 1] == "%"} {
274                    # Magic Button
275                    set ndx [string first "%\]" $para 2]
276                    if {$ndx != -1} {
277                        set button [string range $para 2 [expr $ndx -1]]
278                        set para [string range $para [expr $ndx + 2] end]
279                        lappend result BTN $button
280                        continue
281                    }
282                } elseif {[string index $para 1] == "@"} {
283                    # Left-over embedded macro
284                    set ndx [string first "@\]" $para 2]
285                    if {$ndx != -1} {
286                        set macro [string range $para 2 [expr $ndx -1]]
287                        set para [string range $para [expr $ndx + 2] end]
288                        lappend result MACRO $macro
289                        continue
290                    }
291                } elseif {[string index $para 1] == "!"} {
292                    # Object
293                    set ndx [string first "!\]" $para 2]
294                    if {$ndx != -1} {
295                        set object [string range $para 2 [expr $ndx -1]]
296                        set para [string range $para [expr $ndx + 2] end]
297                        lappend result OBJECT $object
298                        continue
299                    }
300                } else {
301                    # Link
302                    set ndx [string first "\]" $para 1]
303                    if {$ndx != -1} {
304                        set link [string range $para 1 [expr $ndx -1]]
305                        set para [string range $para [expr $ndx + 1] end]
306                        lappend result LINK $link
307                        continue
308                    }
309                }
310            }
311           
312            if {![regexp {^(.[^'<\[]*)(['<\[].*)$} $para dummy elem para]} {
313                set elem $para
314                set para ""
315            }
316           
317            lappend result TXT $elem
318        }
319
320        lappend result /P ""
321    }
322
323   
324    return [linsert $result 0 META [array get meta]]
325}
326
327
328# parseWithHandler text handler
329#
330# text:     The text to parser
331# handler:  A command to handle parsed elements.
332#
333# Parses the input text into tagged values.  The tagname indicates
334# what sort of value it is.  Each tag and value are lappended to the
335# command, which is then called in the global context.
336#
337# debugHandler is a sample handler command; it simply prints each tag
338# and its value to stdout for debugging.
339
340proc ::markupparser::parseWithHandler {text handler} {
341    foreach {tag value} [parse $text] {
342        uplevel \#0 $handler [list $tag $value]
343    }
344}
345
346# At this point, we know the following things:
347# The next line is not blank, with or without extra whitespace.
348# The next line doesn't begin with a #.
349# The end of the paragraph occurs at one of the following boundaries:
350#
351# - A line that begins with "*"
352# - A line that begins with ":"
353# - A line that begins with "#"
354# - A line that looks like "[ \t]*\n"
355# - The end of the text.
356#
357# Returns the index of the end of the current paragraph, or -1 if there
358# is no subsequent paragraph.
359proc ::markupparser::FindEndOfPara {text} {
360    if {![regexp -indices {(\n[ \t]*\n)|(\n\*)|(\n\#)|(\n:)|(\n-)} $text match]} {
361        return -1
362    }
363
364    return [lindex $match 0]
365}
366
367# debugHandler tag value
368#
369# tag:     A parsed markup tag
370# value:   A parsed markup value
371#
372# A simple tag handler; it just outputs its input to stdout for debugging.
373
374proc ::markupparser::debugHandler {tag value} {
375    puts "$tag: <$value>"
376}
377
378# Removes leading and trailing whitespace, and reduces internal whitespace
379# sequences to one space.
380proc ::markupparser::normalizeSpace {text} {
381    regsub -all {\n} $text " " text
382    regsub -all {\s+} $text " " text
383
384    return [string trim $text]
385}
386
387# renameLink text oldName newName
388#
389# Edits text, renaming all links to oldName so that the link to newName
390# instead.
391
392proc ::markupparser::renameLink {text oldName newName} {
393    variable renameResult
394    variable renameOld
395    variable renameNew
396
397    # FIRST, normalize both names.
398    set renameOld [string tolower [normalizeSpace $oldName]]
399    set renameNew [normalizeSpace $newName]
400
401    # Next, do the edit.
402    set renameResult ""
403    foreach {tag value} [parse $text] {
404        switch -exact $tag {
405            META    {}
406            HASH    -
407            PRE     -
408            TCL     -
409            TXT     -
410            BR      -
411            NL   {
412                append renameResult $value
413            }
414            DATA {
415                array set fields $value
416                append renameResult $fields(data)
417            }
418            STY {
419                append renameResult [lindex $value 1]
420            }
421            HTML {
422                append renameResult "<html>$value</html>"
423            }
424            H {
425                set lev [lindex $value 0]
426                set htext [lindex $value 1]
427
428                set tag [string repeat "=" $lev]
429                append renameResult "$tag $htext $tag\n"
430            }
431            P {
432                set ptype [lindex $value 0]
433                set indent [lindex $value 1]
434                set leading [lindex $value 2]
435
436                append renameResult [string repeat $ptype $indent]
437                append renameResult $leading
438            }
439            LINK {
440                if {[regexp {^([^|]+)\|(.*$)} $value dummy linktext name]} {
441                    set page [string tolower [normalizeSpace $name]]
442
443                    if {$page eq $renameOld} {
444                        set value "$linktext|$renameNew"
445                    }
446                } else {
447                    set page [string tolower [normalizeSpace $value]]
448                    if {$page eq $renameOld} {
449                        set value $renameNew
450                    }
451                }
452
453                append renameResult "\[$value\]"
454            }
455            BTN {
456                append renameResult "\[%$value%\]"
457            }
458            MACRO {
459                append renameResult "\[@$value@]"
460            }
461            OBJECT {
462                append renameResult "\[!$value!]"
463            }
464        }
465    }
466    return $renameResult
467}
468
469# formatplain text options
470#
471# Formats the markup into plain text.
472# Styles are ignored, except for <h>...</h>; header text is put in ALL
473# CAPS.  Links are displayed without brackets; magic buttons are displayed
474# by their labels.
475#
476# Options:
477#
478# -pretty flag    If true, macros and errant magic buttons
479#                 are left out.  Defaults to 0
480# -length cols    Line length; defaults to 74.
481
482proc ::markupparser::formatplain {text args} {
483    set option(-pretty) 0
484    set option(-length) 74
485
486    foreach {opt value} $args {
487        if {[info exists option($opt)]} {
488            set option($opt) $value
489        } else {
490            error "Unknown option $opt"
491        }
492    }
493
494    set output ""
495    set bullet 0
496    set indent 0
497    set header 0
498
499    foreach {tag value} [parse $text] {
500        switch $tag {
501            META {}
502            HASH {
503                if {[string match "#---*" $value]} {
504                    append output [string repeat "-" $option(-length)]
505                    append output "\n"
506                }
507            }
508            TCL  -
509            PRE  {
510                append output $value
511            }
512            DATA {
513                array set fields $value
514                append output $fields(data)
515            }
516            P {
517                set header 0
518                set para ""
519                set bullet [expr {[lindex $value 0] eq "*"}]
520                set indent [lindex $value 1]
521            }
522            /P {
523                append output \
524                    [WrapParagraph $para $option(-length) $indent $bullet]
525                append output "\n"
526            }
527            NL {
528                append output "\n"
529            }
530            TXT -
531            HTML {
532                if {$header} {
533                    set value [string toupper $value]
534                }
535                append para $value
536            }
537            BR {
538                append output \
539                    [WrapParagraph $para $option(-length) $indent $bullet]
540                append output "\n"
541                set para ""
542                set bullet 0
543            }
544            H {
545                set lev [lindex $value 0]
546                set htext [lindex $value 1]
547
548                if {$lev == 1} {
549                    set htext [string toupper $htext]
550                }
551                append output "$htext\n"
552            }
553            LINK {
554                if {![regexp {^([^|]+)\|(.*$)} $value dummy linktext name]} {
555                    set linktext $value
556                }
557
558                if {$header} {
559                    set linktext [string toupper $linktext]
560                }
561                append para [normalizeSpace $linktext]
562            }
563            BTN {
564                if {[regexp {^([^|]+)\|(.*$)} $value dummy bname bcode]} {
565                    if {$header} {
566                        set bname [string toupper $bname]
567                    }
568                    append para [normalizeSpace $bname]
569                } elseif {!$option(-pretty)} {
570                    append para "\[%$value%\]"
571                }
572            }
573            MACRO {
574                if {!$option(-pretty)} {
575                    append para "\[@$value@]"
576                }
577            }
578            OBJECT {
579                if {!$option(-pretty)} {
580                    append para "\[!$value!]"
581                }
582            }
583            STY {
584                # Handle header; other styles are ignored.
585                switch -exact [lindex $value 0] {
586                    h1 { set header 1}
587                    h0 { set header 0}
588                }
589            }
590            default {error "Unknown markup tag: $tag"}
591        }
592    }
593
594    return $output
595}
596
597# Wraps the paragraph given the inputs.
598#
599# text      Paragraph to wrap
600# length    Max line length
601# indent    How many tab stops it should be indented.
602# bullet    Whether it's a bulleted paragraph or not.
603
604proc ::markupparser::WrapParagraph {text length indent bullet} {
605    # Wrap it as though it's not indented, but leave space for the indent.
606    set wrapcol [expr {$length - 4*$indent}]
607
608    set wrappedText [::textutil::adjust $text -length $wrapcol]
609
610    if {$indent} {
611        set lines [split $wrappedText "\n"]
612
613        set lead [string repeat "    " $indent]
614
615        if {$bullet} {
616            set first "[string repeat {    } [expr {$indent - 1}]]  * "
617        } else {
618            set first $lead
619        }
620
621        set body [join $lines "\n$lead"]
622
623        set wrappedText $first
624        append wrappedText $body
625    }
626
627    return $wrappedText
628}
629
Note: See TracBrowser for help on using the repository browser.