maui 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (11) hide show
  1. checksums.yaml +7 -0
  2. data/GPL-3 +674 -0
  3. data/Makefile +17 -0
  4. data/Manifest.txt +9 -0
  5. data/README.fab +222 -0
  6. data/README.html +296 -0
  7. data/bin/maui +200 -0
  8. data/lib/mau/fabricator.rb +2071 -0
  9. data/maui.fab +3576 -0
  10. data/maui.gemspec +24 -0
  11. metadata +59 -0
@@ -0,0 +1,3576 @@
1
+ This is Mau Independent Fabricator, a standalone literate
2
+ programming tool with wiki-like notation.
3
+
4
+ << IDENT >>:
5
+ Mau Independent Fabricator << VERSION >>
6
+
7
+ << VERSION >>:
8
+ 3.1.0
9
+
10
+
11
+ == Parsing
12
+
13
+ * Parser's frontend.
14
+
15
+ For the parser, [[input]] will be an open [[IO]] instance. For
16
+ parsing ease (most crucially, in order to support blank lines in
17
+ chunks and indented blocks), we'll wrap it into a
18
+ [[Vertical_Peeker]]. The parser will generate [[OpenStruct]]
19
+ instances for vertical elements extracted from the input, in
20
+ order, and feed them into [[Integrator]] that will take care of
21
+ establishing a proper document tree, collecting the chunks for
22
+ later tangling, and preparing the cross-references.
23
+
24
+ << Parse fabric from [[input]] >>:
25
+
26
+ vp = Fabricator::Vertical_Peeker.new input
27
+ integrator = Fabricator::Integrator.new
28
+
29
+
30
+ The Mau notation has an ambiguity: a line starting with
31
+ whitespace, dash, and whitespace can start an indented block but
32
+ it can also start a bulleted list's item. In order to resolve
33
+ the ambiguity, we'll require that top-level bullet lists be
34
+ unindented, and indented bullet items can only appear if a
35
+ higher-level list has already been established. We'll track
36
+ whether that is the case by the [[in_list]] flag.
37
+
38
+ in_list = false
39
+ loop do
40
+ << Pass and count blank lines >>
41
+ break if vp.eof?
42
+ << Handle explicit section break >>
43
+ element_location = vp.location_ahead
44
+ case vp.peek_line
45
+ << Vertical elements' parsing rules >>
46
+ else raise 'assertion failed'
47
+ end
48
+ integrator.integrate element
49
+ in_list = element.type == :item
50
+ end
51
+ << Handle the end of fabric >>
52
+ integrator.check_chunk_sizes(chunk_size_limit)
53
+
54
+
55
+ << Pass and count blank lines >>:
56
+ vertical_separation = 0
57
+ while vp.peek_line == '' do
58
+ if vertical_separation == 2 then
59
+ integrator.warn vp.location_ahead,
60
+ "more than two consecutive blank lines"
61
+ end
62
+ vertical_separation += 1
63
+ vp.get_line
64
+ end
65
+
66
+
67
+ << Vertical elements' parsing rules >>:
68
+ when /^\s+/ then
69
+ if !in_list or
70
+ vp.peek_line !~ /^
71
+ (?<margin> \s+ )
72
+ - (?<separator> \s+ )
73
+ /x then
74
+ << Parse indented block >>
75
+ else
76
+ << Parse deep bullet >>
77
+ end
78
+
79
+
80
+ What looks to us like an indented block may need to be upgraded
81
+ into a chunk by the integrator. Our parser does not know
82
+ whether this is the case because it does not keep track of
83
+ diversions. Since location data is needed for chunks, we'll
84
+ record it, even if it might not be useful for ordinary indented
85
+ blocks.
86
+
87
+ << Parse indented block >>:
88
+ body_location = vp.location_ahead
89
+ element = vp.get_indented_lines_with_skip
90
+ element.type = :block
91
+ element.body_loc = element_location
92
+
93
+
94
+ << in [[Vertical_Peeker]] >>:
95
+ def get_indented_lines_with_skip
96
+ indent = nil; lines = []
97
+ while peek_line =~ /^\s+/ or
98
+ (peek_line == '' and
99
+ !lines.empty? and
100
+ peek_line(1) =~ /^\s+/) do
101
+ # If the line ahead is not indented but we passed the
102
+ # test, then [[get_line]] will return [[""]] and [[$&]]
103
+ # is the __following__ line's indentation.
104
+ indent = $&.length if indent.nil? or $&.length < indent
105
+ lines.push get_line
106
+ end
107
+ return nil if lines.empty?
108
+ lines.each{|l| l[0 ... indent] = ''}
109
+ return OpenStruct.new(lines: lines, indent: indent)
110
+ end
111
+
112
+
113
+ << Parse deep bullet >>:
114
+ margin = $~['margin']
115
+ lines = [$~['separator'] + $']
116
+ vp.get_line
117
+ while !vp.eof? and
118
+ vp.peek_line.start_with? margin and
119
+ vp.peek_line !~ /^\s*-\s/ do
120
+ lines.push vp.get_line[margin.length .. -1]
121
+ end
122
+ element = OpenStruct.new(
123
+ type: :item,
124
+ lines: lines,
125
+ content: parse_markup(lines.map(&:strip).join ' '),
126
+ indent: margin.length,
127
+ loc: element_location)
128
+
129
+
130
+ A standard chunk header is by its own a divert. However, if
131
+ it's followed by an indented block, it's a chunk.
132
+
133
+ << Vertical elements' parsing rules >>:
134
+
135
+ when /^<<\s*
136
+ (?: (?<root-type> \.file|\.script)\s+ )?
137
+ (?<raw-name> [^\s].*?)
138
+ \s*>>:$/x then
139
+ name = canonicalise_chunk_name $~['raw-name']
140
+ vp.get_line
141
+ element = OpenStruct.new(
142
+ type: :divert,
143
+ root_type: $~['root-type'],
144
+ name: name,
145
+ header_loc: element_location)
146
+
147
+ body_location = vp.location_ahead
148
+ body = vp.get_indented_lines_with_skip
149
+ if body then
150
+ element.type = :chunk
151
+ element.lines = body.lines
152
+ element.indent = body.indent
153
+ element.body_loc = body_location
154
+ element.initial = element.final = true
155
+ end
156
+
157
+
158
+ Next, let's consider top-level bullets.
159
+
160
+ when /^-\s/ then
161
+ # We'll discard the leading dash but save the following
162
+ # whitespace.
163
+ lines = [vp.get_line[1 .. -1]]
164
+ while !vp.eof? and
165
+ vp.peek_line != '' and
166
+ vp.peek_line !~ /^\s*-\s/ do
167
+ lines.push vp.get_line
168
+ end
169
+ element = OpenStruct.new(
170
+ type: :item,
171
+ lines: lines,
172
+ content: parse_markup(lines.map(&:strip).join ' '),
173
+ indent: 0,
174
+ loc: element_location)
175
+
176
+
177
+ Finally, a line starting with a non-whitespace character starts
178
+ an ordinary paragraph, a title, or a rubric.
179
+
180
+ when /^[^\s]/ then
181
+ lines = []
182
+ while vp.peek_line =~ /^[^\s]/ and
183
+ vp.peek_line !~ /^-\s/ do
184
+ lines.push vp.get_line
185
+ end
186
+ mode_flags_to_suppress = 0
187
+ case lines[0]
188
+ << Rules for interpreting a paragraph-like element >>
189
+ end
190
+ element.lines = lines
191
+ element.content =
192
+ parse_markup(lines.map(&:strip).join(' '),
193
+ mode_flags_to_suppress)
194
+
195
+
196
+ << Rules for interpreting a paragraph-like element >>:
197
+
198
+ when /^(==+)(\s+)/ then
199
+ lines[0] = $2 + $'
200
+ element = OpenStruct.new(
201
+ type: :title,
202
+ level: $1.length - 1,
203
+ loc: element_location)
204
+ mode_flags_to_suppress |= Fabricator::MF::LINK
205
+
206
+
207
+ when /^\*\s+/ then
208
+ lines[0] = $'
209
+ element = OpenStruct.new(
210
+ type: :rubric,
211
+ loc: element_location)
212
+
213
+
214
+ else
215
+ element = OpenStruct.new(
216
+ type: :paragraph,
217
+ loc: element_location)
218
+
219
+
220
+ Now, let's go back to the main parsing loop. Seeing two
221
+ consecutive blank lines in front our element triggers an
222
+ explicit section break.
223
+
224
+ << Handle explicit section break >>:
225
+ if vertical_separation >= 2 then
226
+ integrator.force_section_break
227
+ in_list = false
228
+ end
229
+
230
+
231
+ The integrator adds the [[final]] tag to a diverted chunk
232
+ chain's last chunk and checks whether a diversion actually
233
+ applies to any chunks when the chain ends. Inside a fabric,
234
+ this happens by the appearance of certain types of elements --
235
+ chunks, diversions, titles -- but when the fabric is over, we'll
236
+ have to make it explicit.
237
+
238
+ << Handle the end of fabric >>:
239
+
240
+ integrator.clear_diversion
241
+
242
+
243
+ Once we have all the chunks, we can check that the root types of
244
+ all root chunks match each other. It's not (usually) a serious
245
+ error, but the user might want to know.
246
+
247
+ integrator.check_root_type_consistency
248
+
249
+
250
+ For this check, we iterate over chunk headers, so as not to
251
+ generate excessive warnings when the fault lies in a [[divert]].
252
+ (And also, to make sure to generate a warning regarding this if
253
+ a faulty [[divert]] is not actually ever used.)
254
+
255
+ << in [[Integrator]] >>:
256
+ def check_root_type_consistency
257
+ @output.roots.each do |name|
258
+ cbn_entry = @output.chunks_by_name[name]
259
+ effective_root_type = cbn_entry.root_type
260
+ cbn_entry.headers.each do |element|
261
+ unless element.root_type == effective_root_type then
262
+ warn element.header_loc,
263
+ "inconsistent root type, assuming %s" %
264
+ effective_root_type
265
+ end
266
+ end
267
+ end
268
+ return
269
+ end
270
+
271
+
272
+ * Vertical peekaboo.
273
+
274
+ The [[Vertical_Peeker]] class implements a line-level lookahead
275
+ on an [[IO]] instance. We won't limit the lookahead, although
276
+ we'll only need two lines (when checking whether a blank line in
277
+ an indented block is followed by another indented line).
278
+
279
+ << in [[Fabricator]] >>:
280
+ class Vertical_Peeker
281
+ << in [[Vertical_Peeker]] >>
282
+ end
283
+
284
+
285
+ << in [[Vertical_Peeker]] >>:
286
+
287
+ def initialize port
288
+ super()
289
+ @port = port
290
+ if @port.respond_to? :path then
291
+ @filename = @port.path
292
+ elsif @port == $stdin then
293
+ @filename = '(stdin)'
294
+ else
295
+ @filename = '(unknown)'
296
+ end
297
+ @buffer = []
298
+ @line_number = 1 # number of the first line in the buffer
299
+ @eof_seen = false
300
+ return
301
+ end
302
+
303
+
304
+ def peek_line ahead = 0
305
+ raise 'invalid argument' unless ahead >= 0
306
+ until @buffer.length > ahead or @eof_seen do
307
+ line = @port.gets
308
+ if line then
309
+ line.rstrip!
310
+ @buffer.push line
311
+ else
312
+ @eof_seen = true
313
+ end
314
+ end
315
+ return @buffer[ahead] # nil if past eof
316
+ end
317
+
318
+
319
+ def get_line
320
+ # ensure that if a line is available, it's in [[@buffer]]
321
+ peek_line
322
+
323
+ @line_number += 1 unless @buffer.empty?
324
+ return @buffer.shift
325
+ end
326
+
327
+
328
+ def eof?
329
+ return peek_line.nil?
330
+ end
331
+
332
+
333
+ def lineno_ahead
334
+ return @line_number + (@line_consumed ? 1 : 0)
335
+ end
336
+
337
+
338
+ def location_ahead
339
+ return OpenStruct.new(
340
+ filename: @filename, line: lineno_ahead)
341
+ end
342
+
343
+
344
+ * Integration.
345
+
346
+ An [[Integrator]] builds an internal representation of a fabric
347
+ from the vertical elements extracted by a parser.
348
+
349
+ << in [[Fabricator]] >>:
350
+ class Integrator
351
+ << in [[Integrator]] >>
352
+ end
353
+
354
+
355
+ The root of the result, which will be a graph built out of
356
+ [[OpenStruct]]s, can be accessed by the method
357
+ [[Integrator#output]].
358
+
359
+ << in [[Integrator]] >>:
360
+ attr_reader :output
361
+
362
+
363
+ Its top-level structure is evident from the initialisation
364
+ construct.
365
+
366
+ << Initialise [[Integrator@output]] >>:
367
+ @output = OpenStruct.new(
368
+ warnings: [],
369
+ presentation: [], # list of titles and sections
370
+ toc: [],
371
+ chunks_by_name: {},
372
+ # canonical_name => {
373
+ # root_type: String,
374
+ # chunks: list of :chunk/:diverted_chunk records,
375
+ # headers: list of :chunk/:divert records,
376
+ # }
377
+ roots: [], # list of canonical names
378
+ )
379
+
380
+
381
+ The integrator has a number of other internal variables which
382
+ are only used while the fabric is being integrated. In essence,
383
+ they hold a high-level state of the parser. Let's initialise
384
+ them next:
385
+
386
+ << in [[Integrator]] >>:
387
+
388
+ def initialize
389
+ super()
390
+ << Initialise [[Integrator@output]] >>
391
+ @cursec = nil # The current section if started
392
+ @section_count = 0 # The number of last section
393
+ @title_counters = [0]
394
+ @curdivert = nil # The current diversion if active
395
+ @last_divertee = nil
396
+ # last chunk diverted by [[@curdivert]]
397
+ @list_stack = nil
398
+ @in_code = false
399
+ @last_title_level = 0
400
+ @warning_counter = 0
401
+ return
402
+ end
403
+
404
+
405
+ The integrator's main entry point.
406
+
407
+ def integrate element
408
+ if element.type == :title then
409
+ << Integrate the [[title]] element >>
410
+ else
411
+ << Integrate the sub-[[section]] element >>
412
+ end
413
+ return
414
+ end
415
+
416
+
417
+ Title nodes are subject to level restriction, are automatically
418
+ numbered using three-level identifiers, and are in addition to
419
+ [[presentation]] collected into [[toc]].
420
+
421
+ << Integrate the [[title]] element >>:
422
+ # Check the title's level restriction
423
+ if element.level > @last_title_level + 1 then
424
+ warn element.loc, "title level too deep"
425
+ element.level = @last_title_level + 1
426
+ end
427
+ @last_title_level = element.level
428
+
429
+ # Number the title
430
+ while @title_counters.length > element.level do
431
+ @title_counters.pop
432
+ end
433
+ if @title_counters.length < element.level then
434
+ @title_counters.push 0
435
+ end
436
+ @title_counters[-1] += 1
437
+ element.number = @title_counters.join '.'
438
+
439
+ # Append the node to [[presentation]] and [[toc]]
440
+ force_section_break
441
+ @output.presentation.push element
442
+ @output.toc.push element
443
+
444
+ # Enforce (sub(sub))chapter-locality of diversions
445
+ clear_diversion
446
+
447
+
448
+ << Integrate the sub-[[section]] element >>:
449
+ << [[block]] and diverting? Upgrade to [[diverted_chunk]] >>
450
+ << New chunk header? Clear diversion >>
451
+ << ? Break section and warn >>
452
+ << ? Begin new section >>
453
+ << [[rubric]]? Link to [[toc]] >>
454
+ << [[divert]]? Apply it >>
455
+
456
+ if element.type == :item then
457
+ << Integrate the [[item]] element >>
458
+ else
459
+ @cursec.elements.push element
460
+
461
+ if [:chunk, :diverted_chunk].
462
+ include?(element.type) then
463
+ element.section_number = @cursec.section_number
464
+ @in_code = true
465
+ # so we can generate a section break if a
466
+ # narrative-type element follows
467
+ << Parse the chunk's content >>
468
+ end
469
+ << Chunk or divert? Link to [[chunks_by_name]] >>
470
+ @list_stack = nil
471
+ end
472
+
473
+
474
+ An indented block can be either a piece of sample code, which is
475
+ just shown in the woven output, or a headerless code chunk,
476
+ which needs to be tangled, also. Whether it's one or the other
477
+ depends on whether a diversion is in effect; a piece of context
478
+ that the parser does not track. Accordingly, we'll upgrade
479
+ [[block]] nodes to [[diverted_chunk]] nodes in the integrator
480
+ when a diversion is in effect.
481
+
482
+ This is also a convenient place to keep track of the chains of
483
+ diverted chunks. To that end, we'll set the [[initial]] flag if
484
+ the current divert was not used before, and we'll also point
485
+ [[@last_divertee]] to the diverted block so that we can set the
486
+ [[final]] flag on it later.
487
+
488
+ << [[block]] and diverting? Upgrade to [[diverted_chunk]] >>:
489
+ if element.type == :block and @curdivert then
490
+ element.type = :diverted_chunk
491
+ element.name = @curdivert.name
492
+ element.divert = @curdivert
493
+
494
+ element.initial = true if @last_divertee.nil?
495
+ @last_divertee = element
496
+ end
497
+
498
+
499
+ << New chunk header? Clear diversion >>:
500
+ if [:divert, :chunk].include? element.type then
501
+ clear_diversion
502
+ end
503
+
504
+
505
+ As Knuth's original WEB did, we'll generally follow the
506
+ rubric-narrative-code structure in our sections, with the main
507
+ difference being that multiple chunks can appear in a single
508
+ section. A transition from code to narrative thus causes a
509
+ section break.
510
+
511
+ Also, a [[rubric]] causes a section break.
512
+
513
+ In both cases, we'll warn the user if there wasn't an explicit
514
+ section break.
515
+
516
+ << ? Break section and warn >>:
517
+ if (@cursec and element.type == :rubric) or
518
+ (@in_code and
519
+ [:paragraph, :block, :item].include?(
520
+ element.type)) then
521
+ (@cursec.warnings ||= []).push \
522
+ warn(element.loc,
523
+ "silent section break",
524
+ inline: true)
525
+ force_section_break
526
+ end
527
+
528
+
529
+ << ? Begin new section >>:
530
+ if @cursec.nil? then
531
+ @cursec = OpenStruct.new(
532
+ type: :section,
533
+ section_number: (@section_count += 1),
534
+ elements: [])
535
+ @output.presentation.push @cursec
536
+ end
537
+
538
+
539
+ << [[rubric]]? Link to [[toc]] >>:
540
+ if element.type == :rubric then
541
+ element.section_number = @cursec.section_number
542
+ @output.toc.push element
543
+ end
544
+
545
+
546
+ << [[divert]]? Apply it >>:
547
+ if element.type == :divert then
548
+ @curdivert = element
549
+ raise 'assertion failed' unless @last_divertee.nil?
550
+ end
551
+
552
+
553
+ << Integrate the [[item]] element >>:
554
+ # Is this a top-level or descendant item?
555
+ unless @list_stack then
556
+ raise 'assertion failed' unless element.indent == 0
557
+
558
+ # Create a new [[list]] node.
559
+ new_list = OpenStruct.new(
560
+ type: :list,
561
+ items: [],
562
+ indent: element.indent)
563
+ @cursec.elements.push new_list
564
+ @list_stack = [new_list]
565
+ else
566
+ << Discard pending lists deeper than [[element.indent]] >>
567
+ << ? Start a new sublist >>
568
+ end
569
+
570
+ # The list structure has been prepared. Append the
571
+ # new element to the innermost list in progress.
572
+ @list_stack.last.items.push element
573
+
574
+
575
+ << Discard pending lists deeper than [[element.indent]] >>:
576
+ while @list_stack.last.indent > element.indent do
577
+ if @list_stack[-2].indent < element.indent then
578
+ # Unexpected de-dent, like this:
579
+ # - master list
580
+ # - child 1
581
+ # - child 2
582
+ @list_stack.last.indent = element.indent
583
+ (element.warnings ||= []).push \
584
+ warn(element.loc,
585
+ "unexpected dedent", inline: true)
586
+ break
587
+ end
588
+ @list_stack.pop
589
+ end
590
+
591
+
592
+ << ? Start a new sublist >>:
593
+ if @list_stack.last.indent < element.indent then
594
+ if @list_stack.last.sublist then
595
+ raise 'assertion failed'
596
+ end
597
+ new_list = OpenStruct.new(
598
+ type: :list,
599
+ items: [],
600
+ indent: element.indent)
601
+ @list_stack.last.items.last.sublist = new_list
602
+ @list_stack.push new_list
603
+ end
604
+
605
+
606
+ Chunks come out of the parser as series of lines. For
607
+ cross-referencing and tangling, we'll need a slightly more
608
+ complex presentation: a sequence of ([[verbatim]]), reference
609
+ ([[use]] and [[newline]] nodes. This parsing is done by the
610
+ integrator. (It can't be done in the parser because it can't
611
+ tell apart sample code from true chunks if the latter have no
612
+ headers.)
613
+
614
+ Chunk parsing is done by scanning the [[lines]] field of the
615
+ chunk node and constructing a new field, [[content]], along it.
616
+ [[content]] is not line-oriented; rather, it has [[newline]]
617
+ nodes to separate lines. This makes the tangling loop a bit
618
+ easier.
619
+
620
+ << Parse the chunk's content >>:
621
+ element.content = []
622
+ element.lines.each_with_index do
623
+ |line, lineno_in_chunk|
624
+ unless lineno_in_chunk.zero? then
625
+ element.content.push \
626
+ OpenStruct.new(type: :newline)
627
+ end
628
+ << Parse a line of chunk content >>
629
+ end
630
+
631
+
632
+ When parsing chunk lines, we'll want to properly pinpoint the
633
+ locations of [[use]] nodes so that we can issue correct
634
+ warnings. We'll do this by starting from the number of the
635
+ chunk's leftmost column and updating it as we traverse through
636
+ the line, which we'll split using [[String#split]] with a
637
+ capturing regex.
638
+
639
+ << Parse a line of chunk content >>:
640
+ column = 1 + element.indent
641
+ line.split(/(<<\s*
642
+ (?:
643
+ \[\[.*?\]*\]\]
644
+ | .
645
+ )+?
646
+ \s*>>)/x, -1).each_with_index do
647
+ |raw_piece, piece_index|
648
+ << Parse a piece of a line of chunk content >>
649
+ column += raw_piece.length
650
+ end
651
+
652
+ << Parse a piece of a line of chunk content >>:
653
+ node = nil
654
+ if piece_index.odd? then
655
+ << Attempt to parse [[raw_piece]] as a [[use]] node >>
656
+ # If failed, [[node]] is still [[nil]].
657
+ end
658
+ if node.nil? and !raw_piece.empty? then
659
+ node = OpenStruct.new(
660
+ type: :verbatim,
661
+ data: raw_piece)
662
+ end
663
+ element.content.push node if node
664
+
665
+ << Attempt to parse [[raw_piece]] as a [[use]] node >>:
666
+ name = raw_piece[2 ... -2].strip
667
+ # discard the surrounding double brokets
668
+ # together with adjacent whitespace
669
+ node = OpenStruct.new(type: :use,
670
+ name: nil,
671
+ # for ordering; will be replaced below
672
+ raw: raw_piece,
673
+ loc: OpenStruct.new(
674
+ filename: element.body_loc.filename,
675
+ line: element.body_loc.line +
676
+ lineno_in_chunk,
677
+ column: column)
678
+ )
679
+ << Extract affixes from [[name]] into [[node]] >>
680
+ if !name.empty? then
681
+ node.name =
682
+ Fabricator.canonicalise_chunk_name(name)
683
+ else
684
+ # not a proper reference, after all
685
+ node = nil
686
+ end
687
+
688
+ << Extract affixes from [[name]] into [[node]] >>:
689
+ if name =~ /(?:^|\s+)(\|[\w>-]+)$/ and
690
+ Fabricator::POSTPROCESSES.has_key? $1 then
691
+ node.postprocess = $1; name = $`
692
+ end
693
+ if name =~ /(?:^|\s+)(\.dense)$/ then
694
+ node.vertical_separation = $1; name = $`
695
+ end
696
+ if name =~ /^(\.clearindent)(?:\s+|$)/ then
697
+ node.clearindent = true; name = $'
698
+ end
699
+
700
+
701
+ Chunks and diverts we'll be linking under [[chunk_by_name]].
702
+ We'll keep separate track of chunk bodies ([[chunk]] or
703
+ [[diverted_chunk]]), linked under
704
+ [[chunks_by_name[...].chunks]], and chunk headers ([[chunk]] or
705
+ [[divert]]), linked under [[chunks_by_name[...].headers]].
706
+
707
+ << Chunk or divert? Link to [[chunks_by_name]] >>:
708
+ if [:chunk, :diverted_chunk, :divert].include?(
709
+ element.type) then
710
+ cbn_record =
711
+ @output.chunks_by_name[element.name] ||=
712
+ OpenStruct.new(chunks: [], headers: [])
713
+ if [:chunk, :diverted_chunk].include?(
714
+ element.type) then
715
+ cbn_record.chunks.push element
716
+ end
717
+ if [:chunk, :divert].include? element.type then
718
+ cbn_record.headers.push element
719
+ end
720
+
721
+ << Root? Check the filename for sanity >>
722
+
723
+ << Update [[cbn_record.root_type]] from [[element]] >>
724
+ end
725
+
726
+ << Root? Check the filename for sanity >>:
727
+ if element.root_type then
728
+ # check the filename's reasonability
729
+ bad_name = false
730
+ parts = element.name.split '/'
731
+ if ['', '.', '..'].any?{|d| parts.include? d} then
732
+ bad_name = true
733
+ end
734
+ unless parts.all?{|p| p =~ /\A[\w.-]+\Z/} then
735
+ bad_name = true
736
+ end
737
+ if bad_name then
738
+ (element.warnings ||= []).push \
739
+ warn(element.header_loc,
740
+ "unuseable filename",
741
+ inline: true)
742
+ element.root_type = nil
743
+ end
744
+ end
745
+
746
+ << Update [[cbn_record.root_type]] from [[element]] >>:
747
+ # The :chunks_by_name record will hold the highest
748
+ # root_type for chunks of this name, with the order
749
+ # defined as [[nil]] < [['.file']] < [['.script']].
750
+ if element.root_type and
751
+ cbn_record.root_type.nil? then
752
+ cbn_record.root_type = element.root_type
753
+ @output.roots.push element.name
754
+ end
755
+ if element.root_type == '.script' then
756
+ cbn_record.root_type = element.root_type
757
+ end
758
+
759
+
760
+ This concludes the main integration decision tree.
761
+
762
+
763
+ * Integrator's utilities.
764
+
765
+ << in [[Integrator]] >>:
766
+
767
+ def force_section_break
768
+ @cursec = nil
769
+ @list_stack = nil
770
+ @in_code = false
771
+ return
772
+ end
773
+
774
+
775
+ In addition to clearing [[@curdivert]], [[clear_diversion]] also
776
+ sets the [[final]] flag of the last divertee in a chain. In
777
+ order for this to work properly, the main parser calls this, via
778
+ << Handle the end of fabric >>, once the input fabric ends.
779
+
780
+ def clear_diversion
781
+ if @curdivert then
782
+ if !@last_divertee then
783
+ (@curdivert.warnings ||= []).push \
784
+ warn(@curdivert.header_loc,
785
+ "unused diversion",
786
+ inline: true)
787
+ elsif @last_divertee.initial then
788
+ (@curdivert.warnings ||= []).push \
789
+ warn(@curdivert.header_loc,
790
+ "single-use diversion",
791
+ inline: true)
792
+ end
793
+ @curdivert = nil
794
+ @last_divertee.final = true if @last_divertee
795
+ @last_divertee = nil
796
+ end
797
+ return
798
+ end
799
+
800
+
801
+ * Chunk length limit check.
802
+
803
+ << in [[Integrator]] >>:
804
+ def check_chunk_sizes limit
805
+ return unless limit
806
+ @output.presentation.each do |node|
807
+ next unless node.type == :section
808
+ node.elements.each do |element|
809
+ next unless element.type == :chunk
810
+ if element.lines.length > limit then
811
+ if element.lines.length > limit * 2 then
812
+ assessment, factor = "very long chunk", 2
813
+ else
814
+ assessment, factor = "long chunk", 1
815
+ end
816
+ limit_loc = element.body_loc.dup
817
+ limit_loc.column = nil
818
+ limit_loc.line += limit * factor
819
+ (element.warnings ||= []).push \
820
+ warn(limit_loc, "%s (%i lines)" %
821
+ [assessment, element.lines.length],
822
+ inline: true)
823
+ end
824
+ end
825
+ end
826
+ return
827
+ end
828
+
829
+
830
+ * The warning subsystem.
831
+
832
+ Warnings are recorded in the [[warnings]] list of a fabric and
833
+ may also be recorded in individual elements.
834
+ [[Integrator#warn]] generates a record for such storage. Note
835
+ that it does not actually display the warning, for the fabric
836
+ loader doesn't really know anything about the I/O system
837
+ available for this.
838
+
839
+ << in [[Integrator]] >>:
840
+ def warn location, message, inline: false
841
+ record = OpenStruct.new(
842
+ loc: location,
843
+ message: message,
844
+ number: @warning_counter += 1,
845
+ inline: inline)
846
+ @output.warnings.push record
847
+ return record # so it can also be attached elsewhere
848
+ end
849
+
850
+
851
+ In the command line interface, we'll output the warnings once a
852
+ fabric has been fully loaded.
853
+
854
+ << Other methods >>:
855
+ def show_warnings fabric
856
+ fabric.warnings.each do |warning|
857
+ $stderr.puts "%s: %s" %
858
+ [format_location(warning.loc), warning.message]
859
+ end
860
+ return
861
+ end
862
+
863
+
864
+ * Location pinpointing.
865
+
866
+ Locations are encoded using [[OpenStruct]] instances with the
867
+ fields [[filename]] and [[line]] and optionally [[column]].
868
+
869
+ << Other methods >>:
870
+
871
+ def format_location h
872
+ if h.column then
873
+ return "%s:%i.%i" % [h.filename, h.line, h.column]
874
+ else
875
+ return "%s:%i" % [h.filename, h.line]
876
+ end
877
+ end
878
+
879
+
880
+ In order to encode a region's location, we'll save its two
881
+ endpoints, both inclusive, in an [[OpenStruct]] instance as
882
+ [[from]] and [[to]], as standard locations. The [[dash]] is a
883
+ parameter so as to permit n-dash to be used in HTML output.
884
+
885
+ def format_location_range h, dash: "-"
886
+ if h.from.filename != h.to.filename then
887
+ return format_location(h.from) + dash +
888
+ format_location(h.to)
889
+ else
890
+ if h.from.line != h.to.line then
891
+ result = h.from.filename + ":"
892
+ result << h.from.line.to_s
893
+ result << "." << h.from.column.to_s if h.from.column
894
+ result << dash
895
+ result << h.to.line.to_s
896
+ result << "." << h.to.column.to_s if h.to.column
897
+ else
898
+ result = h.from.filename + ":"
899
+ result << h.from.line.to_s
900
+ if h.from.column or h.to.column then
901
+ result << "." <<
902
+ h.from.column.to_s << dash << h.to.column.to_s
903
+ end
904
+ end
905
+ return result
906
+ end
907
+ end
908
+
909
+
910
+ == Horizontal parsing
911
+
912
+ * Chunk name canonicalisation.
913
+
914
+ This amounts to compressing all whitespace that is not marked as
915
+ monospaced by double brackets.
916
+
917
+ << Other methods >>:
918
+ def canonicalise_chunk_name raw_name
919
+ name = ''
920
+ raw_name.strip.split(/(\[\[.*?\]*\]\])/, -1).
921
+ each_with_index do |part, i|
922
+ part.gsub! /\s+/, ' ' if i.even?
923
+ name << part
924
+ end
925
+ return name
926
+ end
927
+
928
+
929
+ * Markup parsing.
930
+
931
+ The markup parser materialises as a function that takes a string
932
+ as its input and returns a list of syntactic nodes. The parser
933
+ itself is a fairly simple linear scanner, the main trick being
934
+ [[Markup_Parser_Stack]] that holds the currently open markup
935
+ constructs.
936
+
937
+ << Other methods >>:
938
+ def parse_markup s, suppress_modes = 0
939
+ ps = Fabricator::Pointered_String.new s
940
+ stack = Fabricator::Markup_Parser_Stack.new suppress_modes
941
+ while ps.pointer < s.length do
942
+ << Parse a bit of markup >>
943
+ end
944
+ while stack.length > 1 do
945
+ stack.unspawn
946
+ end
947
+ return stack.last.content
948
+ end
949
+
950
+
951
+ So, let's consider [[Markup_Parser_Stack]] next. Since it's a
952
+ stack, we'll derive it from [[Array]]:
953
+
954
+ << in [[Fabricator]] >>:
955
+ class Markup_Parser_Stack < Array
956
+ << in [[Markup_Parser_Stack]] >>
957
+ end
958
+
959
+
960
+ At initialisation, the stack will have one frame. We'll
961
+ implement stack frames as [[OpenStruct]] instances. The slot
962
+ [[content]] holds collected markup nodes; [[mode]] is a bitfield
963
+ for currently permitted markup notations, and [[term_type]] is
964
+ the frame marker for unwinding. [[term_type]] normally holds a
965
+ bit, as used in mode, but it is not a bitfield.
966
+
967
+ << in [[Markup_Parser_Stack]] >>:
968
+ def initialize suppress_modes = 0
969
+ super()
970
+ push OpenStruct.new(
971
+ content: [],
972
+ mode: Fabricator::MF::DEFAULTS & ~suppress_modes,
973
+ term_type: 0,
974
+ )
975
+ return
976
+ end
977
+
978
+
979
+ Next, let's list all the bits a [[Markup_Parser_Stack]] frame's
980
+ [[mode]] can hold:
981
+
982
+ << in [[Fabricator]] >>:
983
+ module MF
984
+ BOLD = 0x01
985
+ END_BOLD = 0x02
986
+ ITALIC = 0x04
987
+ END_ITALIC = 0x08
988
+ UNDERSCORE = 0x10
989
+ END_UNDERSCORE = 0x20
990
+ LINK = 0x40
991
+ END_LINK = 0x80
992
+
993
+ DEFAULTS = BOLD | ITALIC | UNDERSCORE | LINK
994
+ end
995
+
996
+
997
+ Whenever the markup parser encounters what looks like a markup
998
+ start notation, it /spawns/ a new frame onto the stack. The new
999
+ frame will contain the notation as [[face]] so that we can
1000
+ [[unspawn]] it, should the start later turn out to be a false
1001
+ start. It overrides the [[mode]] of the top stack frame by
1002
+ enabling the corresponding markup's end notation and disabling
1003
+ another start notation of the same kind. Finally, it tags the
1004
+ new frame with a [[term_type]], which is used in order to find
1005
+ this stack frame when we actually get to the end notation.
1006
+
1007
+ [[OpenStruct]]'s flexibility lets us add extra fields when
1008
+ needed. This is useful for dealing with links, as we'll see
1009
+ later.
1010
+
1011
+ << in [[Markup_Parser_Stack]] >>:
1012
+
1013
+ def spawn face, start_flag, end_flag
1014
+ self.push OpenStruct.new(
1015
+ face: face,
1016
+ content: [],
1017
+ mode: self.last.mode & ~start_flag | end_flag,
1018
+ term_type: end_flag,
1019
+ )
1020
+ return
1021
+ end
1022
+
1023
+
1024
+ The frame can be merged back into its parent frame by
1025
+ /unspawning/ it. This happens when we need to reinterpret what
1026
+ once looked like a start notation as a plain piece of string.
1027
+
1028
+ def unspawn
1029
+ raise 'assertion failed' unless length >= 2
1030
+ top = self.pop
1031
+ self.last.content.push OpenStruct.new(
1032
+ type: :plain,
1033
+ data: top.face,
1034
+ ), *top.content
1035
+ return
1036
+ end
1037
+
1038
+
1039
+ Finally, when we encounter a valid markup end notation, we'll
1040
+ wrap up the current stack frame, /ennoding/ its [[content]] into
1041
+ the matching ancestral stack frame's. Because the markup region
1042
+ may contain a few intervening faux start notations, we may have
1043
+ to first unspawn a few times; the [[frame_type]] parameter will
1044
+ tell [[ennode]] how to recognise the last stack frame to eat up.
1045
+
1046
+ def ennode node_type, frame_type
1047
+ while self.last.term_type != frame_type do
1048
+ self.unspawn
1049
+ end
1050
+ top = self.pop
1051
+ node = OpenStruct.new(
1052
+ type: node_type,
1053
+ content: top.content,
1054
+ )
1055
+ self.last.content.push node
1056
+ return node # for possible further manipulation
1057
+ end
1058
+
1059
+
1060
+ Let's now consider the main markup parsing loop. We'll set it
1061
+ up as a series of [[if ... elsif ... elsif ... end]] clauses.
1062
+
1063
+ The easist markup to parse is undoubtedly [[[[foo]]]] for
1064
+ monospaced text, which doesn't even need a stack frame. The
1065
+ main trick here is that if the terminal [[]]]] contains more
1066
+ than two adjacent closing brackets, we'll pick the two last
1067
+ ones. This trick, originally from Norman Ramsey's [[noweb]],
1068
+ lets the user to use multiple terminal closing brackets in a
1069
+ natural manner, as in [[[[a[b[i]]]]]].
1070
+
1071
+ When parsing the monospaced string, we'll split it into
1072
+ [[plain]] and [[space]] nodes. This is one of the rare cases in
1073
+ Maui's notation when whitespace needs to be recorded, for we'll
1074
+ want to retain sequences of multiple whitespaces in monospaced
1075
+ text.
1076
+
1077
+ << Parse a bit of markup >>:
1078
+ if ps.at? "[[" and
1079
+ end_offset = s.index("]]", ps.pointer + 2) then
1080
+ while ps[end_offset + 2] == ?] do
1081
+ end_offset += 1
1082
+ end
1083
+ monospaced_content = []
1084
+ ps[ps.pointer + 2 ... end_offset].split(/(\s+)/).
1085
+ each_with_index do |part, i|
1086
+ monospaced_content.push OpenStruct.new(
1087
+ type: i.even? ? :plain : :space,
1088
+ data: part
1089
+ )
1090
+ end
1091
+ stack.last.content.push OpenStruct.new(
1092
+ type: :monospace,
1093
+ content: monospaced_content)
1094
+ ps.pointer = end_offset + 2
1095
+
1096
+
1097
+ Detecting the notation for *bold*, /italic/, or _underscore_ is
1098
+ a bit more complicated, so we'll encapsulate it in a separate
1099
+ function. Since it operates on a [[Pointered_String]], we'll
1100
+ insert it into it as an extra method.
1101
+
1102
+ The main rules are:
1103
+ - the starting character is alone, not next to another copy of
1104
+ itself, and
1105
+ - the starter is not immediately followed by a whitespace.
1106
+
1107
+ << in [[Pointered_String]] >>:
1108
+ def biu_starter? c
1109
+ return char_ahead == c &&
1110
+ char_ahead(-1) != c &&
1111
+ ![?\s, c].include?(char_ahead(1))
1112
+ end
1113
+
1114
+
1115
+ When these conditions trigger, we'll establish a new markup
1116
+ parser stack frame:
1117
+
1118
+ << Parse a bit of markup >>:
1119
+ elsif stack.last.mode & Fabricator::MF::BOLD != 0 and
1120
+ ps.biu_starter? ?* then
1121
+ stack.spawn '*',
1122
+ Fabricator::MF::BOLD,
1123
+ Fabricator::MF::END_BOLD
1124
+ ps.pointer += 1
1125
+
1126
+ elsif stack.last.mode & Fabricator::MF::ITALIC != 0 and
1127
+ ps.biu_starter? ?/ then
1128
+ stack.spawn '/',
1129
+ Fabricator::MF::ITALIC,
1130
+ Fabricator::MF::END_ITALIC
1131
+ ps.pointer += 1
1132
+
1133
+ elsif stack.last.mode & Fabricator::MF::UNDERSCORE \
1134
+ != 0 and
1135
+ ps.biu_starter? ?_ then
1136
+ stack.spawn '_',
1137
+ Fabricator::MF::UNDERSCORE,
1138
+ Fabricator::MF::END_UNDERSCORE
1139
+ ps.pointer += 1
1140
+
1141
+
1142
+ The conditions for BIU-terminating markup are similar, except
1143
+ in this case, we'll prohibit whitespace /preceding/ the
1144
+ markup character.
1145
+
1146
+ << in [[Pointered_String]] >>:
1147
+ def biu_terminator? c
1148
+ return char_ahead == c &&
1149
+ char_ahead(1) != c &&
1150
+ ![?\s, c].include?(char_ahead(-1))
1151
+ end
1152
+
1153
+
1154
+ The parser's rules are similarly straightforward, too.
1155
+
1156
+ << Parse a bit of markup >>:
1157
+
1158
+ elsif stack.last.mode & Fabricator::MF::END_BOLD != 0 and
1159
+ ps.biu_terminator? ?* then
1160
+ stack.ennode :bold, Fabricator::MF::END_BOLD
1161
+ ps.pointer += 1
1162
+
1163
+ elsif stack.last.mode & Fabricator::MF::END_ITALIC \
1164
+ != 0 and
1165
+ ps.biu_terminator? ?/ then
1166
+ stack.ennode :italic, Fabricator::MF::END_ITALIC
1167
+ ps.pointer += 1
1168
+
1169
+ elsif stack.last.mode & Fabricator::MF::END_UNDERSCORE \
1170
+ != 0 and
1171
+ ps.biu_terminator? ?_ then
1172
+ stack.ennode :underscore, Fabricator::MF::END_UNDERSCORE
1173
+ ps.pointer += 1
1174
+
1175
+
1176
+ Let's now move on to parsing links.
1177
+
1178
+ Maui's basic link notation is [[<override|target>]], or when
1179
+ override is not desired, just [[<target>]]. This presents a
1180
+ slight conundrum, in that when we pass the initial [[<]], we
1181
+ don't yet know whether we should parse the following as
1182
+ marked-up text -- for the /override/ -- or as a plain string --
1183
+ for /target/. We'll resolve this by beginning parsing as plain
1184
+ text, but storing the offset of the initial broket in the stack
1185
+ frame as [[start_offset]] so we can return to the beginning,
1186
+ should we encounter the terminal [[>]] without an intervening
1187
+ [[|]].
1188
+
1189
+ elsif stack.last.mode & Fabricator::MF::LINK != 0 and
1190
+ ps.biu_starter? ?< then
1191
+ stack.spawn '<',
1192
+ Fabricator::MF::LINK,
1193
+ Fabricator::MF::END_LINK
1194
+ stack.last.start_offset = ps.pointer
1195
+ ps.pointer += 1
1196
+
1197
+
1198
+ When we see [[|]], we have completed parsing the link's
1199
+ override. We can now pick up the plain-text link target,
1200
+ terminated by [[>]], and wrap up the link node.
1201
+
1202
+ elsif stack.last.mode & Fabricator::MF::END_LINK != 0 and
1203
+ ps.at? '|' and
1204
+ end_offset = s.index(?>, ps.pointer + 1) then
1205
+ target = ps[ps.pointer + 1 ... end_offset]
1206
+ if link_like? target then
1207
+ stack.ennode(:link,
1208
+ Fabricator::MF::END_LINK).target = target
1209
+ ps.pointer = end_offset + 1
1210
+ else
1211
+ # False alarm: this is not a link, after all.
1212
+ stack.cancel_link
1213
+ stack.last.content.push OpenStruct.new(
1214
+ type: :plain,
1215
+ data: '|',
1216
+ )
1217
+ ps.pointer += 1
1218
+ end
1219
+
1220
+
1221
+ When we see [[>]] with [[END_LINK]] enabled, we know that
1222
+ this is an unoverridden link. The text between the brokets is
1223
+ now supposed to be plain text link, any markup-like looking
1224
+ symbols such as underscores or slashes notwithstanding, so we'll
1225
+ discard the work we did trying to parse this as markup, and
1226
+ construct a simple link node.
1227
+
1228
+ elsif stack.last.mode & Fabricator::MF::END_LINK != 0 and
1229
+ ps.at? '>' then
1230
+ j = stack.rindex do |x|
1231
+ x.term_type == Fabricator::MF::END_LINK
1232
+ end
1233
+ target = ps[stack[j].start_offset + 1 ... ps.pointer]
1234
+ if link_like? target then
1235
+ stack[j .. -1] = []
1236
+ stack.last.content.push OpenStruct.new(
1237
+ type: :link,
1238
+ implicit_face: true,
1239
+ target: target,
1240
+ content: [OpenStruct.new(
1241
+ type: :plain,
1242
+ data: target,
1243
+ )],
1244
+ )
1245
+ else
1246
+ # False alarm: this is not a link, after all.
1247
+ stack.cancel_link
1248
+ stack.last.content.push OpenStruct.new(
1249
+ type: :plain,
1250
+ data: '>',
1251
+ )
1252
+ end
1253
+ ps.pointer += 1
1254
+
1255
+
1256
+ The [[link_like?]] test lets us avoid misinterpreting some
1257
+ (very) basic broketed constructs such as [[<*>]] --- which may
1258
+ appear when Maui wikitext is generated by copy-pasting
1259
+ ASCII-formatted text --- as links.
1260
+
1261
+ << Other methods >>:
1262
+ def link_like? s
1263
+ return !!(s =~ /\A(?:#\s*)?[[:alnum:]]/)
1264
+ end
1265
+
1266
+
1267
+ When an apparent link turns out to not be a link at all, we'll
1268
+ /cancel/ it. This involves clearing the [[END_LINK]] flag
1269
+ (and the relevant [[term_type]]) in all affected stack frames
1270
+ and restoring the [[LINK]] flag so that we can parse another,
1271
+ more proper link. (Note that we can always restore [[LINK]]:
1272
+ [[cancel_link]] is only called if we were in [[END_LINK]]
1273
+ mode, and this makes only sense if, at a previous point in
1274
+ parsing, we had [[LINK]] set and encountered an opening broket.)
1275
+ We can't perform unwinding at this point because we may have
1276
+ passed over starts of some markup regions which have not ended
1277
+ yet but will end later.
1278
+
1279
+ << in [[Markup_Parser_Stack]] >>:
1280
+ def cancel_link
1281
+ i = self.length
1282
+ begin
1283
+ i -= 1
1284
+ self[i].mode &= ~Fabricator::MF::END_LINK
1285
+ self[i].mode |= Fabricator::MF::LINK
1286
+ end until self[i].term_type == Fabricator::MF::END_LINK
1287
+ self[i].term_type = 0
1288
+ return
1289
+ end
1290
+
1291
+
1292
+ Because whitespace has meaning for Maui markup --- it indicates
1293
+ places suitable for linebreaks when word-wrapping ---, we'll
1294
+ parse it as a kind of special node.
1295
+
1296
+ << Parse a bit of markup >>:
1297
+ elsif ps.at? ' ' then
1298
+ ps.pointer += 1
1299
+ while ps.at? ' ' do
1300
+ ps.pointer += 1
1301
+ end
1302
+ stack.last.content.push OpenStruct.new(type: :space)
1303
+
1304
+
1305
+ We'll consider the [[U+00A0 NO-BREAK SPACE]] a form of markup.
1306
+ Each such character will be a node of the type [[:nbsp]], with
1307
+ no attributes.
1308
+
1309
+ << Parse a bit of markup >>:
1310
+ elsif ps.at? "\u00A0" then
1311
+ stack.last.content.push OpenStruct.new(type: :nbsp)
1312
+ ps.pointer += 1
1313
+
1314
+
1315
+ Finally, if none of the rules matched, we'll pick the following
1316
+ character up as a plain text node. As an optimisation, we'll
1317
+ merge it with any folllowing characters that definitely are not
1318
+ markup characters. Since we don't want the generated node's
1319
+ [[data]] field to retain the type of [[Pointered_String]], we'll
1320
+ construct a new [[String]] instance with its content.
1321
+
1322
+ << Parse a bit of markup >>:
1323
+ else
1324
+ j = ps.pointer + 1
1325
+ while j < s.length and !" */<>[_|".include? ps[j] do
1326
+ j += 1
1327
+ end
1328
+ stack.last.content.push OpenStruct.new(
1329
+ type: :plain,
1330
+ data: String.new(ps[ps.pointer ... j]),
1331
+ )
1332
+ ps.pointer = j
1333
+ end
1334
+
1335
+
1336
+ * String traversal.
1337
+
1338
+ The final part of the markup parser's puzzle is
1339
+ [[Pointered_String]], which provides convenient lookahead (and
1340
+ lookbehind) while traversing the string. We'll derive it from
1341
+ [[String]] by augmenting it with the [[pointer]] field.
1342
+
1343
+ << in [[Fabricator]] >>:
1344
+ class Pointered_String < String
1345
+ def initialize value
1346
+ super value
1347
+ @pointer = 0
1348
+ return
1349
+ end
1350
+
1351
+ attr_accessor :pointer
1352
+
1353
+ << in [[Pointered_String]] >>
1354
+ end
1355
+
1356
+
1357
+ The most basic operation is extracting up to a given number of
1358
+ characters of lookahead.
1359
+
1360
+ << in [[Pointered_String]] >>:
1361
+
1362
+ def ahead length
1363
+ return self[@pointer, length]
1364
+ end
1365
+
1366
+
1367
+ An alternative way of lookahead extracts only a single
1368
+ character, specified via a relative offset from [[@pointer]].
1369
+ The [[delta]] parameter can be negative for lookbehind, and
1370
+ attempts to look before the string begins produce [[nil]] rather
1371
+ than wrapping over to the end of the string.
1372
+
1373
+ def char_ahead delta = 0
1374
+ offset = @pointer + delta
1375
+ return offset >= 0 ? self[offset] : nil
1376
+ end
1377
+
1378
+
1379
+ Parsing decisions often depend on whether the current lookahead
1380
+ matches an etalon. We'll call this operation [[at?]].
1381
+
1382
+ def at? etalon
1383
+ return ahead(etalon.length) == etalon
1384
+ end
1385
+
1386
+
1387
+ There is no point in implementing basic methods for moving
1388
+ [[@pointer]] around, as it has already been fully exposed.
1389
+
1390
+
1391
+ * Construction outside parser.
1392
+
1393
+ In order to simplify constructing the markup structures, we'll
1394
+ subclass [[Array]] into a variant with methods for the markup
1395
+ node types. Calling any such a method results in appending a
1396
+ matching [[OpenStruct]] to the array.
1397
+
1398
+ << Other methods >>:
1399
+ def markup
1400
+ return Fabricator::Markup_Constructor.new
1401
+ end
1402
+
1403
+ << in [[Fabricator]] >>:
1404
+ class Markup_Constructor < Array
1405
+ << in [[Markup_Constructor]] >>
1406
+ end
1407
+
1408
+
1409
+ Let's first write an abstract appender and specialise it later.
1410
+
1411
+ << in [[Markup_Constructor]] >>:
1412
+
1413
+ def node type, **attr
1414
+ return push(OpenStruct.new(type: type, **attr))
1415
+ # [[Array#push]] will return self, allowing [[node]] calls
1416
+ # to be chained.
1417
+ end
1418
+
1419
+
1420
+ def plain data
1421
+ return node(:plain, data: data)
1422
+ end
1423
+
1424
+
1425
+ def space data = nil
1426
+ return node(:space, data: data)
1427
+ end
1428
+
1429
+
1430
+ Now, in order to have some convenience atop our convenience,
1431
+ we'll define the [[words]] method that will parse a given string
1432
+ into 'whitespace' and 'other'. This distinction in the markup
1433
+ is necessary to make sure the whitespace can be broken when
1434
+ word-wrapping the result.
1435
+
1436
+ def words s
1437
+ s.split(/(\s+)/, -1).each_with_index do |part, i|
1438
+ node(i.even? ? :plain : :space, data: part)
1439
+ end
1440
+ return self
1441
+ end
1442
+
1443
+
1444
+ == Tangling
1445
+
1446
+ * The main tangling loop.
1447
+
1448
+ Our tangler does not treat the parsed fabric as a purely passive
1449
+ object; rather, it adds cross-references, annotates chunks with
1450
+ the output locations, and issues warnings over reference
1451
+ problems. Thus, it lives inside the [[Integrator]]. In order
1452
+ to not mislead the programmer to think it's the top-level
1453
+ tangling method, we'll give it a more specific name:
1454
+ [[tangle_chunks]].
1455
+
1456
+ It takes four parameters: [[cbn_entry]] is the
1457
+ [[chunks_by_name]] entry of the node to be tangled (we'll pass a
1458
+ pre-resolved node so as to prevent resolution failure at this
1459
+ point), [[sink]] is the [[Tangling_Sink]] instance to use,
1460
+ [[trace]] is a [[Set]] used to detect circular references, and
1461
+ [[vsep]] can override the number of linebreaks to be generated
1462
+ between adjacent chunks. By default, we'll leave one blank
1463
+ line, hence two linebreaks. The [[.dense]] option removes the
1464
+ blank line, leaving only one separating linebreak.
1465
+
1466
+ During tangling, we'll collect each chunk's tangling locations.
1467
+ (Note that it's possible that one chunk will be tangled into
1468
+ multiple places). We'll store this data into the chunk as the
1469
+ [[tangle_locs]] list. We'll also collect tangling locations of
1470
+ chunk chains -- sequences of [[diverted_chunk]] elements
1471
+ diverted by the same [[divert]]. We'll store this data into the
1472
+ [[divert]] node as the [[chain_tangle_locs]] list.
1473
+
1474
+ << in [[Integrator]] >>:
1475
+ def tangle_chunks cbn_entry, sink, trace, vsep = 2
1476
+ chain_start_loc = nil
1477
+ cbn_entry.chunks.each_with_index do |chunk, i|
1478
+ vsep.times{sink.newline} unless i.zero?
1479
+ if chunk.divert and chunk.initial then
1480
+ raise 'assertion failed' if chain_start_loc
1481
+ chain_start_loc = sink.location_ahead
1482
+ end
1483
+ << Tangle [[chunk]] to [[sink]] >>
1484
+ if chunk.divert and chunk.final then
1485
+ raise 'assertion failed' unless chain_start_loc
1486
+ (chunk.divert.chain_tangle_locs ||= []).push \
1487
+ OpenStruct.new(
1488
+ from: chain_start_loc,
1489
+ to: sink.location_behind)
1490
+ chain_start_loc = nil
1491
+ end
1492
+ end
1493
+ return
1494
+ end
1495
+
1496
+
1497
+ In addition to expanding the chunk's content to [[sink]], we'll
1498
+ also record its location in the output file, as kept track by
1499
+ [[Tangling_Sink]].
1500
+
1501
+ << Tangle [[chunk]] to [[sink]] >>:
1502
+ start_location = sink.location_ahead
1503
+ chunk.content.each do |node|
1504
+ case node.type
1505
+ when :verbatim then
1506
+ sink.write node.data
1507
+ when :newline then
1508
+ sink.newline
1509
+ when :use then
1510
+ tangle_transclusion node, sink, trace, chunk
1511
+ else raise 'data structure error'
1512
+ end
1513
+ end
1514
+ end_location = sink.location_behind
1515
+
1516
+ # Both endpoints are inclusive.
1517
+ (chunk.tangle_locs ||= []).push OpenStruct.new(
1518
+ from: start_location,
1519
+ to: end_location)
1520
+
1521
+
1522
+ For circular or dangling references, we'll write the raw
1523
+ transclusion directive, as it appeared in the fabric, to output.
1524
+ It's possible that we mistakenly parsed something that was not
1525
+ intended as a reference, and while this should be fixed by
1526
+ appropriate escaping (or, well, dividing the bogus reference
1527
+ onto multiple code lines), this non-destructive approach is
1528
+ probably optimal as workarounds go.
1529
+
1530
+ << in [[Integrator]] >>:
1531
+ def tangle_transclusion node, sink, trace, referrer
1532
+ name = node.name
1533
+ if trace.include? name then
1534
+ warn node.loc, "circular reference"
1535
+ sink.write node.raw
1536
+ else
1537
+ cbn_entry = @output.chunks_by_name[name]
1538
+ if cbn_entry.nil? or cbn_entry.chunks.empty? then
1539
+ warn node.loc, "dangling reference"
1540
+ sink.write node.raw
1541
+ else
1542
+ << Cross-reference the transclusion >>
1543
+ << Recurse and transclude >>
1544
+ end
1545
+ end
1546
+ return
1547
+ end
1548
+
1549
+
1550
+ << Cross-reference the transclusion >>:
1551
+ (cbn_entry.transcluders ||= []).push(
1552
+ OpenStruct.new(
1553
+ name: referrer.name,
1554
+ section_number: referrer.section_number,
1555
+ ))
1556
+
1557
+
1558
+ << Recurse and transclude >>:
1559
+ trace.add name
1560
+ if node.postprocess then
1561
+ # redirect the tangler
1562
+ outer_sink = sink
1563
+ inner_sport = StringIO.new
1564
+ sink = Fabricator::Tangling_Sink.new '(pipe)',
1565
+ inner_sport
1566
+ end
1567
+ sink.pin_indent node.clearindent ? 0 : nil do
1568
+ tangle_chunks cbn_entry, sink, trace,
1569
+ node.vertical_separation == '.dense' ? 1 : 2
1570
+ end
1571
+ if node.postprocess then
1572
+ # revert the redirect and apply the filter
1573
+ sink.newline
1574
+ filter_output =
1575
+ Fabricator::POSTPROCESSES[node.postprocess].
1576
+ call(inner_sport.string)
1577
+ sink = outer_sink
1578
+ sink.pin_indent node.clearindent ? 0 : nil do
1579
+ sink.write_long filter_output
1580
+ end
1581
+ end
1582
+ trace.delete name
1583
+
1584
+
1585
+ << in [[Fabricator]] >>:
1586
+ POSTPROCESSES = {
1587
+ '|scss->css' => proc do |input|
1588
+ require 'sass'
1589
+ Sass::Engine.new(input,
1590
+ syntax: :scss,
1591
+ load_paths: [],
1592
+ filename: '(pipe)').render
1593
+ end,
1594
+
1595
+ '|sass->css' => proc do |input|
1596
+ require 'sass'
1597
+ Sass::Engine.new(input,
1598
+ syntax: :sass,
1599
+ load_paths: [],
1600
+ filename: '(pipe)').render
1601
+ end,
1602
+ }
1603
+
1604
+
1605
+ * Tangle all the files.
1606
+
1607
+ The high-level tangling interface, [[Integrator#tangle_roots]],
1608
+ sets up the [[tangles]] branch in the fabric. (For idempotency,
1609
+ it does nothing if it already exists. This is mainly useful
1610
+ because it calls [[tangle_chunks]] which has the side effect of
1611
+ inserting cross-reference information into the fabric.)
1612
+
1613
+ << in [[Integrator]] >>:
1614
+ def tangle_roots
1615
+ return if @output.tangles
1616
+ @output.tangles = {}
1617
+ @output.roots.each do |name|
1618
+ sport = StringIO.new
1619
+ sink = Fabricator::Tangling_Sink.new name, sport
1620
+ cbn_entry = @output.chunks_by_name[name]
1621
+ # We can assume that [[cbn_entry]] is not [[nil]], for
1622
+ # otherwise there wouldn't be a [[roots]] entry.
1623
+ tangle_chunks cbn_entry, sink, Set.new([name])
1624
+ sink.newline
1625
+ @output.tangles[name] = OpenStruct.new(
1626
+ filename: name,
1627
+ root_type: cbn_entry.root_type,
1628
+ content: sport.string,
1629
+ line_count: sink.line_count,
1630
+ nonblank_line_count: sink.nonblank_line_count,
1631
+ longest_line_length: sink.longest_line_length,
1632
+ )
1633
+ end
1634
+ return
1635
+ end
1636
+
1637
+
1638
+ Finally, at the command line interface level, we'll just need to
1639
+ call [[tangle_roots]] and write the results into files. The
1640
+ latter is done through the [[writeout_plan]] abstraction layer.
1641
+
1642
+ << Tangle all roots >>:
1643
+ integrator.tangle_roots
1644
+
1645
+ << Other methods >>:
1646
+ # Take a [[results]] record from tangling and construct a
1647
+ # matching [[proc]] to be stored in the [[writeout_plan]].
1648
+ def plan_to_write_out results
1649
+ return proc do |output_filename|
1650
+ File.write output_filename, results.content
1651
+ puts "Tangled #{results.filename},"
1652
+ if results.line_count != 1 then
1653
+ print " #{results.line_count} lines"
1654
+ else
1655
+ print " #{results.line_count} line"
1656
+ end
1657
+ puts " (#{results.nonblank_line_count} non-blank),"
1658
+ if results.longest_line_length != 1 then
1659
+ puts " longest #{results.longest_line_length} chars."
1660
+ else
1661
+ puts " longest #{results.longest_line_length} char."
1662
+ end
1663
+ << Script root? Make executable >>
1664
+ end
1665
+ end
1666
+
1667
+
1668
+ << Script root? Make executable >>:
1669
+ if results.root_type == '.script' and
1670
+ !Fabricator::WINDOWS_HOSTED_P then
1671
+ stat = File.stat output_filename
1672
+ m = stat.mode
1673
+ uc = ""
1674
+ [(m |= 0o100), (uc << "u")] if m & 0o400 != 0
1675
+ [(m |= 0o010), (uc << "g")] if m & 0o040 != 0
1676
+ [(m |= 0o001), (uc << "o")] if m & 0o004 != 0
1677
+ File.chmod m, output_filename
1678
+ puts "Set %s+x on %s, resulting in %03o" % [
1679
+ uc,
1680
+ output_filename,
1681
+ m & 0o777,
1682
+ ]
1683
+ end
1684
+
1685
+ << in [[Fabricator]] >>:
1686
+ WINDOWS_HOSTED_P =
1687
+ (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
1688
+
1689
+
1690
+ === The tangling sink.
1691
+
1692
+ * The tangling sink.
1693
+
1694
+ The tangling sink serves as a backend to the tangling loop,
1695
+ taking care of indentation, right-stripping the generated lines,
1696
+ and keeping track of the output location so as to permit
1697
+ cross-referencing chunks with where they appear in the generated
1698
+ files.
1699
+
1700
+ << in [[Fabricator]] >>:
1701
+ class Tangling_Sink
1702
+ def initialize filename, port
1703
+ super()
1704
+ @filename = filename
1705
+ @port = port
1706
+ @lineno = 1
1707
+ @line = ''
1708
+ @indent = 0
1709
+ << Initialise statistical trackers >>
1710
+ return
1711
+ end
1712
+
1713
+ << in [[Tangling_Sink]] >>
1714
+ end
1715
+
1716
+ << in [[Tangling_Sink]] >>:
1717
+
1718
+
1719
+ * Simple output.
1720
+
1721
+ def write s
1722
+ @line << s
1723
+ return
1724
+ end
1725
+
1726
+
1727
+ def newline
1728
+ @line.rstrip!
1729
+ @port.puts @line
1730
+ @lineno += 1
1731
+ << Count [[@line]] for statistics >>
1732
+ @line = ' ' * @indent
1733
+ return
1734
+ end
1735
+
1736
+
1737
+ We'll use Ruby's block idiom for indentation. By default,
1738
+ [[pin_indent]] will retain the current column as the beginning
1739
+ column for lines beginning during the thunk passed to it. The
1740
+ amount of indentation can be overridden by passing it to
1741
+ [[pin_indent]] as a parameter. (The parameter of [[nil]] can be
1742
+ used to explicitly request the implicit behaviour.) Such an
1743
+ override is used to implement the [[.clearindent]] directive.
1744
+
1745
+ def pin_indent level = nil
1746
+ previous_indent = @indent
1747
+ begin
1748
+ @indent = level || @line.length
1749
+ yield
1750
+ ensure
1751
+ @indent = previous_indent
1752
+ end
1753
+ return
1754
+ end
1755
+
1756
+
1757
+ A filter's output is written into a sink as a long multiline
1758
+ string. Note that we'll ignore trailing linebreaks in such a
1759
+ string.
1760
+
1761
+ def write_long s
1762
+ s.split(/\n/).each_with_index do |line, i|
1763
+ newline unless i.zero?
1764
+ write line
1765
+ end
1766
+ return
1767
+ end
1768
+
1769
+
1770
+ * Location tracking.
1771
+
1772
+ def location_ahead
1773
+ return OpenStruct.new(
1774
+ filename: @filename,
1775
+ line: @lineno,
1776
+ column: @line.length + 1)
1777
+ end
1778
+
1779
+ def location_behind
1780
+ return OpenStruct.new(
1781
+ filename: @filename,
1782
+ line: @lineno,
1783
+ column: @line.length)
1784
+ end
1785
+
1786
+
1787
+ * And finally, statistics.
1788
+
1789
+ The number of output lines is easily calculated from the line
1790
+ number tracker.
1791
+
1792
+ def line_count
1793
+ return @lineno - 1
1794
+ end
1795
+
1796
+
1797
+ The number of processed sections does not even need any
1798
+ calculation.
1799
+
1800
+ << in [[Integrator]] >>:
1801
+ attr_reader :section_count
1802
+
1803
+
1804
+ In order to count non-blank lines, we need a separate counter.
1805
+
1806
+ << Initialise statistical trackers >>:
1807
+ @nonblank_line_count = 0
1808
+
1809
+ << in [[Tangling_Sink]] >>:
1810
+ attr_reader :nonblank_line_count
1811
+
1812
+ << Count [[@line]] for statistics >>:
1813
+ @nonblank_line_count += 1 unless @line.empty?
1814
+
1815
+
1816
+ We also want to find out the generated file's width.
1817
+
1818
+ << Initialise statistical trackers >>:
1819
+ @longest_line_length = 0
1820
+
1821
+ << in [[Tangling_Sink]] >>:
1822
+ attr_reader :longest_line_length
1823
+
1824
+ << Count [[@line]] for statistics >>:
1825
+ @longest_line_length = @line.length \
1826
+ if @line.length > @longest_line_length
1827
+
1828
+ == Fabric loader's façade
1829
+
1830
+ For user's convenience, we'll encapsulate the parsing,
1831
+ integration, and tangling of a fabric into a single procedure.
1832
+
1833
+ << Other methods >>:
1834
+ def load_fabric input, chunk_size_limit: 24
1835
+ << Parse fabric from [[input]] >>
1836
+ << Tangle all roots >>
1837
+ return integrator.output
1838
+ end
1839
+
1840
+ == Weaving
1841
+
1842
+ === Coloured text
1843
+
1844
+ * Coloured text.
1845
+
1846
+ This output mode is convenient for use in a modern (x)terminal.
1847
+
1848
+ << Weave [[fabric]] (ctxt) >>:
1849
+ open filename, 'w' do |port|
1850
+ Fabricator.weave_ctxt fabric, port,
1851
+ width: $cmdline.output_width,
1852
+ pseudographics: $cmdline.pseudographics
1853
+ end
1854
+ puts "Weaved #{filename}"
1855
+
1856
+ << Other methods >>:
1857
+ def weave_ctxt fabric, port,
1858
+ width: 80,
1859
+ pseudographics: Fabricator::UNICODE_PSEUDOGRAPHICS
1860
+ wr = Fabricator::Text_Wrapper.new port,
1861
+ width: width,
1862
+ pseudographics: pseudographics
1863
+ << Weave fabric's warnings (ctxt) >>
1864
+ << Weave presentation (ctxt) >>
1865
+ return
1866
+ end
1867
+
1868
+ << Weave fabric's warnings (ctxt) >>:
1869
+ unless fabric.warnings.empty? then
1870
+ wr.styled :section_title do
1871
+ wr.add_plain 'Warnings'
1872
+ end
1873
+ wr.linebreak
1874
+ wr.linebreak
1875
+ weave_ctxt_warning_list fabric.warnings, wr
1876
+ wr.linebreak
1877
+ end
1878
+
1879
+
1880
+ Since warning lists can be output not only in the beginning of
1881
+ the woven fabric but also inline, we'll implement this as a
1882
+ procedure.
1883
+
1884
+ We'll want to be able to pass any random node's [[.warnings]] to
1885
+ [[weave_ctxt_warning_list]] even if there aren't any warnings.
1886
+ To that end, we'll first pass the [[list]] parameter through
1887
+ [[#to_a]], which is a no-op for proper arrays but maps [[nil]]
1888
+ to an empty list.
1889
+
1890
+ << Other methods >>:
1891
+ def weave_ctxt_warning_list list, wr, inline: false,
1892
+ indent: true
1893
+ list.to_a.each do |warning|
1894
+ wr.styled inline ? :inline_warning : :null do
1895
+ wr.add_plain (indent ? ' ' : '') + '!!! ' if inline
1896
+ wr.add_plain format_location(warning.loc)
1897
+ wr.add_plain ':'
1898
+ wr.add_space
1899
+ wr.hang do
1900
+ warning.message.split(/(\s+)/).
1901
+ each_with_index do |part, i|
1902
+ if i.even? then
1903
+ wr.add_plain part
1904
+ else
1905
+ wr.add_space part
1906
+ end
1907
+ end
1908
+ end
1909
+ end
1910
+ wr.linebreak
1911
+ end
1912
+ return
1913
+ end
1914
+
1915
+
1916
+ << Weave presentation (ctxt) >>:
1917
+ toc_generated = false
1918
+ fabric.presentation.each do |element|
1919
+ case element.type
1920
+ when :title then
1921
+ << Weave the [[title]] node (ctxt) >>
1922
+ when :section then
1923
+ << Weave the [[section]] (ctxt) >>
1924
+ else raise 'data structure error'
1925
+ end
1926
+ end
1927
+
1928
+
1929
+ << Weave the [[section]] (ctxt) >>:
1930
+ rubricated = element.elements[0].type == :rubric
1931
+ # If we're encountering the first rubric/title, output
1932
+ # the table of contents.
1933
+ if rubricated and !toc_generated then
1934
+ weave_ctxt_toc fabric.toc, wr
1935
+ toc_generated = true
1936
+ end
1937
+
1938
+ << Weave the section's lead and set [[start_index]] (ctxt) >>
1939
+
1940
+ << Weave the section's body from [[start_index]] on (ctxt) >>
1941
+
1942
+ << Weave the section's top-level warnings (ctxt) >>
1943
+
1944
+
1945
+ Weaving a section is a bit tricky, for we want to pretend that
1946
+ both the section number, which is a field of a [[section]] node,
1947
+ and the rubric (if any), which is optionally the first child of
1948
+ it, comprise the section's title. Furthermore, following
1949
+ Knuth's practice, we'll want both to precede the first regular
1950
+ paragraph, if any, without a separating paragraph break.
1951
+
1952
+ If the section has a rubric, we'll paint both the section number
1953
+ and the rubric red (style [[:rubric]]) If there's no rubric,
1954
+ the section number will be just bold ([[:section_number]], to be
1955
+ precise). This distinction corresponds to the Knuth's original
1956
+ (C)WEB's practice of distinguishing 'starred sections' from
1957
+ plain ones.
1958
+
1959
+ << Weave the section's lead and set [[start_index]] (ctxt) >>:
1960
+ start_index = 0 # index of the first non-special child
1961
+ << Output section number and, possibly, the rubric (ctxt) >>
1962
+
1963
+ # If the rubric or the section sign is followed by a
1964
+ # paragraph, a chunk header, or a divert, we'll output
1965
+ # it in the same paragraph.
1966
+ starter = element.elements[start_index]
1967
+ if starter then
1968
+ case starter.type
1969
+ when :paragraph, :divert, :chunk then
1970
+ wr.add_space
1971
+ weave_ctxt_section_part starter, fabric, wr
1972
+ start_index += 1
1973
+ else
1974
+ wr.linebreak
1975
+ end
1976
+ end
1977
+
1978
+ # Finally, the blank line that separates the special
1979
+ # paragraph from the section's body, if any.
1980
+ wr.linebreak
1981
+
1982
+ << Output section number and, possibly, the rubric (ctxt) >>:
1983
+ if rubricated then
1984
+ start_index += 1
1985
+ wr.styled :rubric do
1986
+ wr.add_plain "§%i." % element.section_number
1987
+ wr.add_space
1988
+ wr.add_nodes element.elements.first.content
1989
+ end
1990
+ else
1991
+ wr.styled :section_number do
1992
+ wr.add_plain "§%i." % element.section_number
1993
+ end
1994
+ end
1995
+
1996
+ << Weave the section's body from [[start_index]] on (ctxt) >>:
1997
+ element.elements[start_index .. -1].each do |child|
1998
+ weave_ctxt_section_part child, fabric, wr
1999
+ wr.linebreak
2000
+ end
2001
+
2002
+ << Other methods >>:
2003
+ def weave_ctxt_section_part element, fabric, wr
2004
+ case element.type
2005
+ << [[weave_ctxt_section_part]] rules >>
2006
+ else
2007
+ raise 'data structure error'
2008
+ end
2009
+ return
2010
+ end
2011
+
2012
+
2013
+ << [[weave_ctxt_section_part]] rules >>:
2014
+
2015
+ when :paragraph then
2016
+ wr.add_nodes element.content
2017
+ wr.linebreak
2018
+
2019
+
2020
+ when :divert, :chunk, :diverted_chunk then
2021
+ if [:divert, :chunk].include? element.type then
2022
+ weave_ctxt_chunk_header element, wr
2023
+ weave_ctxt_warning_list element.warnings, wr,
2024
+ inline: true
2025
+ end
2026
+ if [:chunk, :diverted_chunk].include? element.type then
2027
+ << Weave the chunk's lines (ctxt) >>
2028
+ << ? Output the chain's finality marker >>
2029
+ weave_ctxt_warning_list element.warnings, wr,
2030
+ inline: true
2031
+ if element.final then
2032
+ wr.styled :chunk_xref do
2033
+ wr.add_nodes xref_chain(element, fabric)
2034
+ end
2035
+ wr.linebreak
2036
+ end
2037
+ end
2038
+
2039
+
2040
+ when :list then
2041
+ weave_ctxt_list element.items, wr
2042
+
2043
+
2044
+ when :block then
2045
+ weave_ctxt_block element, wr
2046
+
2047
+
2048
+ << Weave the section's top-level warnings (ctxt) >>:
2049
+ unless (element.warnings || []).empty? then
2050
+ weave_ctxt_warning_list element.warnings, wr,
2051
+ inline: true, indent: false
2052
+ wr.linebreak
2053
+ end
2054
+
2055
+
2056
+ * Weaving code elements.
2057
+
2058
+ This method is good for both chunks with headers and diverts,
2059
+ which in its regard, are just chunk headers.
2060
+
2061
+ << Other methods >>:
2062
+
2063
+ def weave_ctxt_chunk_header element, wr
2064
+ wr.styled :chunk_header do
2065
+ wr.add_pseudographics :before_chunk_name
2066
+ if element.root_type then
2067
+ wr.styled :root_type do
2068
+ wr.add_plain element.root_type
2069
+ end
2070
+ wr.add_space
2071
+ end
2072
+ wr.add_nodes(
2073
+ parse_markup(element.name, Fabricator::MF::LINK))
2074
+ wr.add_pseudographics :after_chunk_name
2075
+ wr.add_plain ":"
2076
+ end
2077
+ wr.linebreak
2078
+ return
2079
+ end
2080
+
2081
+
2082
+ def weave_ctxt_block element, wr
2083
+ element.lines.each do |line|
2084
+ wr.styled :block_frame do
2085
+ wr.add_pseudographics :block_margin
2086
+ end
2087
+ wr.styled :monospace do
2088
+ wr.add_plain line
2089
+ end
2090
+ wr.linebreak
2091
+ end
2092
+ return
2093
+ end
2094
+
2095
+
2096
+ << Weave the chunk's lines (ctxt) >>:
2097
+ wr.styled :chunk_frame do
2098
+ wr.add_pseudographics element.initial ?
2099
+ :initial_chunk_margin :
2100
+ :chunk_margin
2101
+ end
2102
+ wr.styled :monospace do
2103
+ element.content.each do |node|
2104
+ case node.type
2105
+ when :verbatim then
2106
+ wr.add_plain node.data
2107
+ when :newline then
2108
+ wr.linebreak
2109
+ wr.styled :chunk_frame do
2110
+ wr.add_pseudographics :chunk_margin
2111
+ end
2112
+ when :use then
2113
+ weave_ctxt_use node, wr
2114
+ else raise 'data structure error'
2115
+ end
2116
+ end
2117
+ end
2118
+ wr.linebreak
2119
+
2120
+
2121
+ << Other methods >>:
2122
+ def weave_ctxt_use node, wr
2123
+ wr.styled :use do
2124
+ wr.add_pseudographics :before_chunk_name
2125
+ if node.clearindent then
2126
+ wr.add_plain ".clearindent "
2127
+ end
2128
+ wr.add_nodes parse_markup(node.name, Fabricator::MF::LINK)
2129
+ if node.vertical_separation then
2130
+ wr.add_plain " " + node.vertical_separation
2131
+ end
2132
+ if node.postprocess then
2133
+ wr.add_plain " " + node.postprocess
2134
+ end
2135
+ wr.add_pseudographics :after_chunk_name
2136
+ end
2137
+ return
2138
+ end
2139
+
2140
+
2141
+ << ? Output the chain's finality marker >>:
2142
+ if element.final then
2143
+ wr.styled :chunk_frame do
2144
+ wr.add_pseudographics :final_chunk_marker
2145
+ end
2146
+ wr.linebreak
2147
+ end
2148
+
2149
+
2150
+ << Other methods >>:
2151
+ # Given a chunk, prepare its transclusion summary as a list of
2152
+ # markup nodes. Should only be used on chunks that are the
2153
+ # last in a chunk chain (i.e., that have [[final]] set).
2154
+ def xref_chain element, fabric, dash: "-"
2155
+ xref = markup
2156
+ if element.initial then
2157
+ xref.words "This chunk is "
2158
+ else
2159
+ xref.words "These chunks are "
2160
+ end
2161
+ << Summarise chunk's referrers into [[xref]] >>
2162
+ xref.words " and "
2163
+ << List chunk's tangled locations into [[xref]] >>
2164
+ return xref
2165
+ end
2166
+
2167
+ << Summarise chunk's referrers into [[xref]] >>:
2168
+ cbn_entry = fabric.chunks_by_name[element.name]
2169
+ transcluders = cbn_entry.transcluders
2170
+ if transcluders then
2171
+ xref.words "transcluded by "
2172
+ xref.push *commatise_oxfordly(
2173
+ transcluders.map{|ref| markup.
2174
+ node(:mention_chunk, name: ref.name).
2175
+ space.
2176
+ plain("(§%i)" % ref.section_number)
2177
+ })
2178
+ else
2179
+ if cbn_entry.root_type then
2180
+ xref.words "solely a transclusion root"
2181
+ else
2182
+ xref.words "never transcluded"
2183
+ end
2184
+ end
2185
+
2186
+ << List chunk's tangled locations into [[xref]] >>:
2187
+ tlocs = element.divert ?
2188
+ element.divert.chain_tangle_locs :
2189
+ element.tangle_locs
2190
+ if tlocs then
2191
+ xref.
2192
+ words("tangled to ").
2193
+ push(*commatise_oxfordly(
2194
+ tlocs.map{|range| markup.
2195
+ plain(format_location_range(range, dash: dash))
2196
+ })).
2197
+ plain(".")
2198
+ else
2199
+ xref.words "never tangled."
2200
+ end
2201
+
2202
+
2203
+ This contraption generates lists with Oxford/Harvard-style
2204
+ commas. The input is a list of lists of markup nodes. The
2205
+ output is a single list of markup nodes, with appropriate
2206
+ joiners interspersed.
2207
+
2208
+ << Other methods >>:
2209
+ def commatise_oxfordly items
2210
+ result = []
2211
+ items.each_with_index do |item, i|
2212
+ unless i.zero? then
2213
+ unless items.length == 2 then
2214
+ result.push OpenStruct.new(:type => :plain,
2215
+ :data => ',')
2216
+ end
2217
+ result.push OpenStruct.new(:type => :space)
2218
+ if i == items.length - 1 then
2219
+ result.push OpenStruct.new(:type => :plain,
2220
+ :data => 'and')
2221
+ result.push OpenStruct.new(:type => :space)
2222
+ end
2223
+ end
2224
+ result.push *item
2225
+ end
2226
+ return result
2227
+ end
2228
+
2229
+
2230
+ * Weaving other narrative elements.
2231
+
2232
+ << Weave the [[title]] node (ctxt) >>:
2233
+ if !toc_generated then
2234
+ weave_ctxt_toc fabric.toc, wr
2235
+ toc_generated = true
2236
+ end
2237
+ wr.styled :section_title do
2238
+ wr.add_plain "#{element.number}."
2239
+ wr.add_space
2240
+ wr.hang do
2241
+ wr.add_nodes element.content
2242
+ end
2243
+ end
2244
+ wr.linebreak
2245
+ wr.linebreak
2246
+
2247
+
2248
+ << Other methods >>:
2249
+
2250
+ def weave_ctxt_list items, wr
2251
+ items.each do |item|
2252
+ wr.add_pseudographics :bullet
2253
+ wr.add_plain " "
2254
+ wr.hang do
2255
+ wr.add_nodes item.content
2256
+ end
2257
+ wr.linebreak
2258
+ unless (item.warnings || []).empty? then
2259
+ wr.hang do
2260
+ weave_ctxt_warning_list item.warnings, wr,
2261
+ inline: true
2262
+ end
2263
+ end
2264
+ if item.sublist then
2265
+ wr.add_plain " "
2266
+ wr.hang do
2267
+ weave_ctxt_list item.sublist.items, wr
2268
+ end
2269
+ end
2270
+ end
2271
+ return
2272
+ end
2273
+
2274
+
2275
+ def weave_ctxt_toc toc, wr
2276
+ if toc.length >= 2 then
2277
+ wr.styled :section_title do
2278
+ wr.add_plain 'Contents'
2279
+ end
2280
+ wr.linebreak; wr.linebreak
2281
+ rubric_level = 0
2282
+ toc.each do |entry|
2283
+ << Weave the TOC entry (ctxt) >>
2284
+ end
2285
+ wr.linebreak
2286
+ end
2287
+ return
2288
+ end
2289
+
2290
+
2291
+ << Weave the TOC entry (ctxt) >>:
2292
+ case entry.type
2293
+ when :title then
2294
+ rubric_level = entry.level - 1 + 1
2295
+ wr.add_plain ' ' * (entry.level - 1)
2296
+ wr.add_plain entry.number + '.'
2297
+ wr.add_space
2298
+ wr.hang do
2299
+ wr.add_nodes entry.content
2300
+ end
2301
+
2302
+ when :rubric then
2303
+ wr.add_plain ' ' * rubric_level
2304
+ wr.add_plain '§%i.' % entry.section_number
2305
+ wr.add_space
2306
+ wr.hang do
2307
+ wr.add_nodes entry.content
2308
+ end
2309
+
2310
+ else
2311
+ raise 'assertion failed'
2312
+ end
2313
+ wr.linebreak
2314
+
2315
+
2316
+ * The text wrapper.
2317
+
2318
+ We'll word-wrap the narrative (but not code) to a specified
2319
+ output width using [[Text_Wrapper]]. It also accounts for the
2320
+ escape sequences for colours, mainly by considering that their
2321
+ width, for word-wrapping purposes, is zero.
2322
+
2323
+ << in [[Fabricator]] >>:
2324
+ class Text_Wrapper
2325
+ def initialize port = $stdout,
2326
+ width: 80,
2327
+ pseudographics: UNICODE_PSEUDOGRAPHICS,
2328
+ palette: DEFAULT_PALETTE
2329
+ super()
2330
+ @port = port
2331
+ @width = width
2332
+ @pseudographics = pseudographics
2333
+ @palette = palette
2334
+ @hangindent = 0
2335
+ @curpos = 0
2336
+ @curspace = nil
2337
+ @curword = OpenStruct.new(
2338
+ prepared_output: '',
2339
+ width: 0)
2340
+ @curmode = @palette.null
2341
+ return
2342
+ end
2343
+
2344
+ << in [[Text_Wrapper]] >>
2345
+ end
2346
+
2347
+
2348
+ << in [[Text_Wrapper]] >>:
2349
+
2350
+ Process a series of 'word' characters. Note that these do not
2351
+ have to comprise a whole word; a word can be fed into the
2352
+ [[Text_Wrapper]] using multiple consecutive [[add_plain]] calls.
2353
+ All word characters, even whitespaces among them, are considered
2354
+ nonbreakable.
2355
+
2356
+ def add_plain data
2357
+ if @curspace and @curpos + data.length > @width then
2358
+ # the space becomes a linebreak
2359
+ @port.puts @palette.null
2360
+ @port.print ' ' * @hangindent + @curmode
2361
+ @curspace = nil
2362
+ @curpos = @hangindent + @curword.width
2363
+ end
2364
+ @curword.prepared_output << data
2365
+ @curpos += data.length
2366
+ return
2367
+ end
2368
+
2369
+
2370
+ Process a space character (or several, or zero). This sets up a
2371
+ permitted line break point.
2372
+
2373
+ def add_space data = ' '
2374
+ @port.print @curspace.prepared_output if @curspace
2375
+ @port.print @curword.prepared_output
2376
+ @curspace = OpenStruct.new(
2377
+ prepared_output: data,
2378
+ width: data.length)
2379
+ @curword = OpenStruct.new(
2380
+ prepared_output: '',
2381
+ width: 0)
2382
+ @curpos += data.length
2383
+ return
2384
+ end
2385
+
2386
+
2387
+ Explicit linebreak.
2388
+
2389
+ def linebreak
2390
+ @port.print @curspace.prepared_output if @curspace
2391
+ @port.print @curword.prepared_output
2392
+ @port.puts @palette.null
2393
+ @port.print ' ' * @hangindent + @curmode
2394
+ @curspace = nil
2395
+ @curword = OpenStruct.new(
2396
+ prepared_output: '',
2397
+ width: 0)
2398
+ @curpos = @hangindent
2399
+ return
2400
+ end
2401
+
2402
+
2403
+ Process a node, as generated by [[parse_markup]].
2404
+
2405
+ def add_node node
2406
+ case node.type
2407
+ when :plain then
2408
+ add_plain node.data
2409
+ when :space then
2410
+ add_space node.data || ' '
2411
+ when :nbsp then
2412
+ add_plain ' '
2413
+ when :monospace, :bold, :italic, :underscore then
2414
+ styled node.type do
2415
+ add_nodes node.content
2416
+ end
2417
+ when :mention_chunk then
2418
+ add_pseudographics :before_chunk_name
2419
+ add_nodes(
2420
+ Fabricator.parse_markup(node.name,
2421
+ Fabricator::MF::LINK))
2422
+ add_pseudographics :after_chunk_name
2423
+ when :link then
2424
+ if node.implicit_face then
2425
+ styled :link do
2426
+ add_plain '<'
2427
+ add_nodes node.content
2428
+ add_plain '>'
2429
+ end
2430
+ else
2431
+ add_plain '<'
2432
+ add_nodes node.content
2433
+ unless node.implicit_face then
2434
+ add_space ' '
2435
+ styled :link do
2436
+ add_plain node.target
2437
+ end
2438
+ end
2439
+ add_plain '>'
2440
+ end
2441
+ else
2442
+ # Uh-oh, a bug: the parser generated a node of a type
2443
+ # unknown to the weaver.
2444
+ raise 'invalid node type'
2445
+ end
2446
+ return
2447
+ end
2448
+
2449
+
2450
+ Process a whole list of nodes.
2451
+
2452
+ def add_nodes nodes
2453
+ nodes.each do |node|
2454
+ add_node node
2455
+ end
2456
+ return
2457
+ end
2458
+
2459
+
2460
+ Hanging indentation. Used, for example, in the table of content
2461
+ and in lists. The content is to be fed into the
2462
+ [[Text_Wrapper]] by the block supplied by the caller.
2463
+
2464
+ def hang
2465
+ # convert the preceding whitespace, if any, into 'hard'
2466
+ # space not subject to future wrapping
2467
+ if @curspace then
2468
+ @port.print @curspace.prepared_output
2469
+ @curspace = nil
2470
+ end
2471
+ prev_hangindent = @hangindent
2472
+ begin
2473
+ @hangindent = @curpos
2474
+ yield
2475
+ ensure
2476
+ @hangindent = prev_hangindent
2477
+ end
2478
+ return
2479
+ end
2480
+
2481
+
2482
+ A region wrapped in an escape sequence. The sequence is looked
2483
+ up in [[@palette]], is treated as having zero width, and it gets
2484
+ turned off (using the [[:null]] style) during linebreaks; see
2485
+ [[add_plain]]. The region's content is to be fed into the
2486
+ [[Text_Wrapper]] by the block supplied by the caller.
2487
+
2488
+ Note that [[styled]] calls can be nested, but only the innermost
2489
+ style is restored after linebreaks.
2490
+
2491
+ def styled sequence_name
2492
+ sequence = @palette[sequence_name]
2493
+ raise 'unknown palette entry' unless sequence
2494
+ prev_mode = @curmode
2495
+ begin
2496
+ @curmode = sequence
2497
+ @curword.prepared_output << sequence
2498
+ yield
2499
+ ensure
2500
+ @curmode = prev_mode
2501
+ @curword.prepared_output << prev_mode
2502
+ end
2503
+ return
2504
+ end
2505
+
2506
+
2507
+ * Pseudographics.
2508
+
2509
+ We'll mark code chunks with running vertical lines on the left,
2510
+ with a turn at the head or tail to indicate whether this chunk
2511
+ is initial or final in its chain. This is best done using box
2512
+ graphics, for which we'll use Unicode, but for archaic devices,
2513
+ we'll also support plain ASCII box graphics.
2514
+
2515
+ << in [[Fabricator]] >>:
2516
+
2517
+ UNICODE_PSEUDOGRAPHICS = OpenStruct.new(
2518
+ bullet: [0x2022].pack('U*'),
2519
+ before_chunk_name: [0x00AB].pack('U*'),
2520
+ after_chunk_name: [0x00BB].pack('U*'),
2521
+ initial_chunk_margin: [0x2500, 0x2510].pack('U*'),
2522
+ chunk_margin: [0x0020, 0x2502].pack('U*'),
2523
+ block_margin: " ",
2524
+ final_chunk_marker:
2525
+ ([0x0020, 0x2514] + [0x2500] * 3).pack('U*'),
2526
+ )
2527
+
2528
+
2529
+ ASCII_PSEUDOGRAPHICS = OpenStruct.new(
2530
+ bullet: "-",
2531
+ before_chunk_name: "<<",
2532
+ after_chunk_name: ">>",
2533
+ initial_chunk_margin: "+ ",
2534
+ chunk_margin: "| ",
2535
+ block_margin: " ",
2536
+ final_chunk_marker: "----",
2537
+ )
2538
+
2539
+
2540
+ As implied before, the default is Unicode.
2541
+
2542
+ << Initialise [[$cmdline]] >>:
2543
+ $cmdline.pseudographics = Fabricator::UNICODE_PSEUDOGRAPHICS
2544
+
2545
+
2546
+ Client code can output the pseudographics by this method.
2547
+
2548
+ << in [[Text_Wrapper]] >>:
2549
+ def add_pseudographics name
2550
+ seq = @pseudographics[name]
2551
+ raise 'unknown pseudographics item' unless seq
2552
+ add_plain seq
2553
+ return
2554
+ end
2555
+
2556
+
2557
+ * Palette.
2558
+
2559
+ << in [[Fabricator]] >>:
2560
+ DEFAULT_PALETTE = OpenStruct.new(
2561
+ monospace: "\e[38;5;71m",
2562
+ bold: "\e[1m",
2563
+ italic: "\e[3m",
2564
+ underscore: "\e[4m",
2565
+ root_type: "\e[4m",
2566
+ chunk_frame: "\e[38;5;59m",
2567
+ block_frame: "",
2568
+ chunk_xref: "\e[38;5;59;3m",
2569
+ section_title: "\e[1;48;5;17m",
2570
+ # unspecified intense on dark blue background
2571
+ rubric: "\e[31;1m",
2572
+ section_number: "\e[0;1m",
2573
+ chunk_header: "\e[0;33;1m",
2574
+ use: "\e[34;1m",
2575
+ null: "\e[0m",
2576
+ inline_warning: "\e[31m",
2577
+ link: "\e[38;5;32m",
2578
+ )
2579
+
2580
+
2581
+ === HTML
2582
+
2583
+ * Overview.
2584
+
2585
+ First, let's take care of the I/O.
2586
+
2587
+ << Weave [[fabric]] (HTML) >>:
2588
+ open filename, 'w' do |port|
2589
+ port.set_encoding 'utf-8'
2590
+ Fabricator.weave_html fabric, port,
2591
+ title: $cmdline.fabric_filename,
2592
+ link_css: $cmdline.link_css
2593
+ end
2594
+ puts "Weaved #{filename}"
2595
+
2596
+
2597
+ The next issue is the basic HTML structure. We'll follow HTML 5
2598
+ conventions.
2599
+
2600
+ << Other methods >>:
2601
+ def weave_html fabric, port,
2602
+ title: nil,
2603
+ link_css: []
2604
+ title ||= "(Untitled)"
2605
+ port.puts '<!doctype html>'
2606
+ port.puts '<html>'
2607
+ << Generate woven HTML's [[head]] >>
2608
+ port.puts '<body>'
2609
+ port.puts
2610
+ port.puts "<h1>#{title.to_xml}</h1>"
2611
+ << Weave fabric's warnings (HTML) >>
2612
+ << Weave presentation (HTML) >>
2613
+ port.puts '</html>'
2614
+ port.puts '</body>'
2615
+ port.puts '</html>'
2616
+ return
2617
+ end
2618
+
2619
+ << Generate woven HTML's [[head]] >>:
2620
+ port.puts '<head>'
2621
+ port.puts "<meta http-equiv='Content-type' " +
2622
+ "content='text/html; charset=utf-8' />"
2623
+ port.puts "<title>#{title.to_xml}</title>"
2624
+ if link_css.empty? then
2625
+ port.puts "<style type='text/css'>"
2626
+ port.puts '<< .clearindent maui.scss |scss->css >>'
2627
+ port.puts "</style>"
2628
+ else
2629
+ link_css.each do |link|
2630
+ port.puts ("<link rel='stylesheet' type='text/css' " +
2631
+ "href='%s' />") % link.to_xml
2632
+ end
2633
+ end
2634
+ port.puts '</head>'
2635
+
2636
+ << Weave presentation (HTML) >>:
2637
+ toc_generated = false
2638
+ fabric.presentation.each do |element|
2639
+ case element.type
2640
+ when :title then
2641
+ << Weave the [[title]] node (HTML) >>
2642
+ when :section then
2643
+ << Weave the [[section]] (HTML) >>
2644
+ else raise 'data structure error'
2645
+ end
2646
+ port.puts
2647
+ end
2648
+
2649
+ << Weave the [[title]] node (HTML) >>:
2650
+ if !toc_generated then
2651
+ weave_html_toc fabric.toc, port
2652
+ toc_generated = true
2653
+ end
2654
+ port.print '<h%i' % (element.level + 1)
2655
+ port.print " id='%s'" % "T.#{element.number}"
2656
+ port.print '>'
2657
+ port.print "#{element.number}. "
2658
+ htmlify element.content, port
2659
+ port.puts '</h%i>' % (element.level + 1)
2660
+
2661
+ << Weave the [[section]] (HTML) >>:
2662
+ rubricated = element.elements[0].type == :rubric
2663
+ # If we're encountering the first rubric/title, output
2664
+ # the table of contents.
2665
+ if rubricated and !toc_generated then
2666
+ weave_html_toc fabric.toc, port
2667
+ toc_generated = true
2668
+ end
2669
+
2670
+ start_index = 0
2671
+ port.puts "<section class='maui-section' id='%s'>" %
2672
+ "S.#{element.section_number}"
2673
+ port.puts
2674
+ << Weave the [[section]]'s lead (HTML) >>
2675
+ port.puts
2676
+ element.elements[start_index .. -1].each do |child|
2677
+ weave_html_section_part child, fabric, port
2678
+ port.puts
2679
+ end
2680
+ << Weave the section's top-level warnings, if any (HTML) >>
2681
+ port.puts "</section>"
2682
+
2683
+ << Weave the [[section]]'s lead (HTML) >>:
2684
+ port.print "<p>"
2685
+ << Weave section's number and rubric (HTML) >>
2686
+ subelement = element.elements[start_index]
2687
+ warnings = nil
2688
+ case subelement && subelement.type
2689
+ when :paragraph then
2690
+ port.print " "
2691
+ htmlify subelement.content, port
2692
+ start_index += 1
2693
+ when :divert then
2694
+ port.print " "
2695
+ weave_html_chunk_header subelement, 'maui-divert',
2696
+ port, tag: 'span'
2697
+ warnings = subelement.warnings
2698
+ start_index += 1
2699
+ end
2700
+ port.puts "</p>"
2701
+ if warnings then
2702
+ weave_html_warning_list warnings, port, inline: true
2703
+ end
2704
+
2705
+ << Weave section's number and rubric (HTML) >>:
2706
+ port.print "<b class='%s'>" %
2707
+ (rubricated ? 'maui-rubric' : 'maui-section-number')
2708
+ port.print "\u00A7#{element.section_number}."
2709
+ if rubricated then
2710
+ port.print " "
2711
+ htmlify element.elements[start_index].content, port
2712
+ start_index += 1
2713
+ end
2714
+ port.print "</b>"
2715
+
2716
+
2717
+ << Other methods >>:
2718
+ def weave_html_section_part element, fabric, port
2719
+ case element.type
2720
+ << [[weave_html_section_part]] rules >>
2721
+ else
2722
+ raise 'data structure error'
2723
+ end
2724
+ return
2725
+ end
2726
+
2727
+
2728
+ << [[weave_html_section_part]] rules >>:
2729
+
2730
+ when :paragraph then
2731
+ port.print "<p>"
2732
+ htmlify element.content, port
2733
+ port.puts "</p>"
2734
+
2735
+
2736
+ when :list then
2737
+ weave_html_list element.items, port
2738
+
2739
+
2740
+ when :divert then
2741
+ weave_html_chunk_header element, 'maui-divert',
2742
+ port
2743
+ port.puts
2744
+ weave_html_warning_list element.warnings, port,
2745
+ inline: true
2746
+
2747
+
2748
+ when :chunk, :diverted_chunk then
2749
+ port.print "<div class='maui-chunk"
2750
+ port.print " maui-initial-chunk" if element.initial
2751
+ port.print " maui-final-chunk" if element.final
2752
+ port.print "'>"
2753
+ if element.type == :chunk then
2754
+ weave_html_chunk_header element, 'maui-chunk-header',
2755
+ port
2756
+ port.puts
2757
+ end
2758
+ weave_html_chunk_body element, port
2759
+ unless (element.warnings || []).empty? then
2760
+ weave_html_warning_list element.warnings, port,
2761
+ inline: true
2762
+ end
2763
+ if element.final then
2764
+ port.print "<div class='maui-chunk-xref'>"
2765
+ htmlify(
2766
+ xref_chain(element, fabric, dash: "\u2013"),
2767
+ port)
2768
+ port.puts "</div>"
2769
+ end
2770
+ port.puts "</div>"
2771
+
2772
+
2773
+ when :block then
2774
+ port.print "<pre class='maui-block'>"
2775
+ element.lines.each_with_index do |line, i|
2776
+ port.puts unless i.zero?
2777
+ port.print line.to_xml
2778
+ end
2779
+ port.puts "</pre>"
2780
+
2781
+
2782
+ As in the coloured text output, the table of content takes a
2783
+ general form of a tree. However, there's a new feature: we're
2784
+ going to make each entry into a local link, either in the
2785
+ [[#T.foo]] form for titles or [[#S.foo]] form for sections.
2786
+
2787
+ << Other methods >>:
2788
+ def weave_html_toc toc, port
2789
+ if toc.length >= 2 then
2790
+ port.puts "<h2>Contents</h2>"
2791
+ port.puts
2792
+ last_level = 0
2793
+ # What level should the rubrics in the current
2794
+ # (sub(sub))chapter appear at?
2795
+ rubric_level = 1
2796
+ toc.each do |entry|
2797
+ if entry.type == :rubric then
2798
+ level = rubric_level
2799
+ else
2800
+ level = entry.level
2801
+ rubric_level = entry.level + 1
2802
+ end
2803
+ << Generate [[ul]]/[[li]] tags to match [[level]] >>
2804
+ << Weave the TOC entry (HTML) >>
2805
+ last_level = level
2806
+ end
2807
+ port.puts "</li></ul>" * last_level
2808
+ port.puts
2809
+ end
2810
+ return
2811
+ end
2812
+
2813
+ << Generate [[ul]]/[[li]] tags to match [[level]] >>:
2814
+ if level > last_level then
2815
+ raise 'assertion failed' \
2816
+ unless level == last_level + 1
2817
+ port.print "\n<ul><li>"
2818
+ elsif level == last_level then
2819
+ port.print "</li>\n<li>"
2820
+ else
2821
+ port.print "</li></ul>" * (last_level - level) +
2822
+ "\n<li>"
2823
+ end
2824
+
2825
+ << Weave the TOC entry (HTML) >>:
2826
+ case entry.type
2827
+ when :title then
2828
+ port.print "#{entry.number}. "
2829
+ port.print "<a href='#T.#{entry.number}'>"
2830
+ htmlify entry.content, port
2831
+ port.print "</a>"
2832
+ when :rubric then
2833
+ port.print "\u00A7#{entry.section_number}. "
2834
+ port.print "<a href='#S.#{entry.section_number}'>"
2835
+ htmlify entry.content, port
2836
+ port.print "</a>"
2837
+ else
2838
+ raise 'assertion failed'
2839
+ end
2840
+
2841
+ << Other methods >>:
2842
+
2843
+ def weave_html_list items, port
2844
+ port.puts "<ul>"
2845
+ items.each do |item|
2846
+ port.print "<li>"
2847
+ htmlify item.content, port
2848
+ if item.sublist then
2849
+ port.puts
2850
+ weave_html_list item.sublist.items, port
2851
+ end
2852
+ unless (item.warnings || []).empty? then
2853
+ port.puts
2854
+ weave_html_warning_list item.warnings, port,
2855
+ inline: true
2856
+ end
2857
+ port.puts "</li>"
2858
+ end
2859
+ port.puts "</ul>"
2860
+ return
2861
+ end
2862
+
2863
+
2864
+ def weave_html_chunk_header element, cls, port, tag: 'div'
2865
+ port.print "<#{tag} class='%s'>" % cls
2866
+ port.print "&#xAB;"
2867
+ if element.root_type then
2868
+ port.print "<u>%s</u> " % element.root_type.to_xml
2869
+ end
2870
+ htmlify(
2871
+ parse_markup(element.name, Fabricator::MF::LINK),
2872
+ port)
2873
+ port.print "&#xBB;:"
2874
+ port.print "</#{tag}>"
2875
+ # Note that we won't output a trailing linebreak here.
2876
+ return
2877
+ end
2878
+
2879
+
2880
+ def weave_html_chunk_body element, port
2881
+ port.print "<pre class='maui-chunk-body'>"
2882
+ element.content.each do |node|
2883
+ case node.type
2884
+ when :verbatim then
2885
+ port.print node.data.to_xml
2886
+ when :newline then
2887
+ port.puts
2888
+ when :use then
2889
+ << Weave the [[use]] node (HTML) >>
2890
+ else raise 'data structure error'
2891
+ end
2892
+ end
2893
+ port.puts "</pre>"
2894
+ return
2895
+ end
2896
+
2897
+ << Weave the [[use]] node (HTML) >>:
2898
+ port.print "<span class='maui-transclude'>"
2899
+ port.print "&#xAB;"
2900
+ if node.clearindent then
2901
+ port.print ".clearindent "
2902
+ end
2903
+ htmlify(
2904
+ parse_markup(node.name, Fabricator::MF::LINK),
2905
+ port)
2906
+ if node.vertical_separation then
2907
+ port.print " " + node.vertical_separation.to_xml
2908
+ end
2909
+ if node.postprocess then
2910
+ port.print " " + node.postprocess.to_xml
2911
+ end
2912
+ port.print "&#xBB;"
2913
+ port.print "</span>"
2914
+
2915
+
2916
+ * Displaying warnings in HTML.
2917
+
2918
+ << Weave the section's top-level warnings, if any (HTML) >>:
2919
+ unless (element.warnings || []).empty? then
2920
+ weave_html_warning_list element.warnings, port,
2921
+ inline: true
2922
+ port.puts
2923
+ end
2924
+
2925
+ << Weave fabric's warnings (HTML) >>:
2926
+ unless fabric.warnings.empty? then
2927
+ port.puts "<h2>Warnings</h2>"
2928
+ port.puts
2929
+ weave_html_warning_list fabric.warnings, port
2930
+ port.puts
2931
+ end
2932
+
2933
+ << Other methods >>:
2934
+ def weave_html_warning_list list, port, inline: false
2935
+ if list and !list.empty? then
2936
+ port.print "<ul class='maui-warnings"
2937
+ port.print " maui-inline-warnings" if inline
2938
+ port.puts "'>"
2939
+ list.each do |warning|
2940
+ port.print "<li"
2941
+ port.print " id='W.#{warning.number}'" if inline
2942
+ port.print ">"
2943
+ port.print "!!! " if inline
2944
+ if !inline and warning.inline then
2945
+ port.print "<a href='#W.%i'>" % warning.number
2946
+ end
2947
+ port.print "<tt>%s</tt>" %
2948
+ format_location(warning.loc).to_xml
2949
+ port.print ": " + warning.message
2950
+ port.print "</a>" if !inline and warning.inline
2951
+ port.puts "</li>"
2952
+ end
2953
+ port.puts "</ul>"
2954
+ end
2955
+ return
2956
+ end
2957
+
2958
+
2959
+ * Conversion of a horizontal markup tree to HTML.
2960
+
2961
+ << Other methods >>:
2962
+ def htmlify nodes, port
2963
+ nodes.each do |node|
2964
+ case node.type
2965
+ << [[case]] clauses of [[htmlify]] >>
2966
+ else
2967
+ raise 'invalid node type'
2968
+ end
2969
+ end
2970
+ return
2971
+ end
2972
+
2973
+
2974
+ << [[case]] clauses of [[htmlify]] >>:
2975
+
2976
+ when :plain then
2977
+ port.print node.data.to_xml
2978
+
2979
+
2980
+ when :space then
2981
+ port.print((node.data || ' ').to_xml)
2982
+
2983
+
2984
+ when :nbsp then
2985
+ port.print '&nbsp;'
2986
+
2987
+
2988
+ when :monospace, :bold, :italic, :underscore then
2989
+ html_tag = Fabricator::MARKUP2HTML[node.type]
2990
+ port.print "<%s>" % html_tag
2991
+ htmlify node.content, port
2992
+ port.print "</%s>" % html_tag
2993
+
2994
+
2995
+ when :mention_chunk then
2996
+ port.print "<span class='maui-chunk-mention'>\u00AB"
2997
+ htmlify(
2998
+ parse_markup(node.name, Fabricator::MF::LINK),
2999
+ port)
3000
+ port.print "\u00BB</span>"
3001
+
3002
+
3003
+ when :link then
3004
+ port.print "<a href='#{node.target.to_xml}'>"
3005
+ htmlify node.content, port
3006
+ port.print "</a>"
3007
+
3008
+
3009
+ << in [[Fabricator]] >>:
3010
+ MARKUP2HTML = {
3011
+ :monospace => 'code',
3012
+ :bold => 'b',
3013
+ :italic => 'i',
3014
+ :underscore => 'u',
3015
+ }
3016
+
3017
+
3018
+ * XML escaping.
3019
+
3020
+ << Outer definitions >>:
3021
+ class ::String
3022
+ # Local enclosed variable for [[#to_xml]]
3023
+ char_entities = {
3024
+ '&' => '&amp;',
3025
+ '<' => '&lt;',
3026
+ '>' => '&gt;',
3027
+ '"' => '&quot;',
3028
+ "'" => '&apos;',
3029
+ }.freeze
3030
+
3031
+ define_method :to_xml do ||
3032
+ return gsub(/[&<>'"]/){char_entities[$&]}
3033
+ end
3034
+ end
3035
+
3036
+
3037
+ * The built-in stylesheet.
3038
+
3039
+ Our built-in stylesheet has been written in SCSS. Maui runs it
3040
+ through Sass to get plain CSS at tangling time.
3041
+
3042
+ << maui.scss >>:
3043
+ /**** Fonts ****/
3044
+ << SCSS [[@import]] clauses for Google fonts >>
3045
+
3046
+ // Dimensions
3047
+ << SCSS dimensions >>
3048
+
3049
+ // Colours
3050
+ << SCSS colours >>
3051
+
3052
+ /**** Rules ****/
3053
+ << SCSS rules >>
3054
+
3055
+
3056
+ Maui's HTML-woven output uses two font families: a 'plain text'
3057
+ one for most of the narrative and a 'monospaced' one for actual
3058
+ code. We have chosen Roboto for the former and Cousine for the
3059
+ latter.
3060
+
3061
+ << SCSS [[@import]] clauses for Google fonts >>:
3062
+ $fontsrc: "http://fonts.googleapis.com/css?family=";
3063
+ @import url("#{$fontsrc}Roboto");
3064
+ @import url("#{$fontsrc}Cousine");
3065
+
3066
+ << SCSS rules >>:
3067
+ body, .maui-transclude {
3068
+ font-family: "Roboto", sans-serif;
3069
+ }
3070
+
3071
+ pre, tt, code {
3072
+ font-family: "Cousine", monospace;
3073
+ }
3074
+
3075
+
3076
+ The main text for Maui's output will be black on white.
3077
+
3078
+ << SCSS colours >>:
3079
+ $main-foreground: black;
3080
+ $main-background: white;
3081
+
3082
+ << SCSS rules >>:
3083
+ body {
3084
+ colour: $main-foreground;
3085
+ background: $main-background;
3086
+ }
3087
+
3088
+
3089
+ Furthermore, when monospaced text appears in narrative (as
3090
+ contrary to code chunks), we'll dye it green for extra
3091
+ highlighting.
3092
+
3093
+ << SCSS colours >>:
3094
+ $narrative-monospaced-colour: forestgreen;
3095
+
3096
+ << SCSS rules >>:
3097
+ tt, code {
3098
+ color: $narrative-monospaced-colour;
3099
+ }
3100
+
3101
+
3102
+ Inline warnings we'll paint bright red for high visibility.
3103
+ (Note that the class' name is in plural --- a single HTML
3104
+ element can contain multiple warnings.)
3105
+
3106
+ << SCSS colours >>:
3107
+ $inline-warning-colour: red;
3108
+
3109
+ << SCSS rules >>:
3110
+ .maui-inline-warnings {
3111
+ color: $inline-warning-colour;
3112
+ }
3113
+
3114
+
3115
+ The rules so far have a conflict. What happens when a [[tt]]
3116
+ element appears inside an inline warning? Left alone, they
3117
+ would become green. However, in this context, we'll want them
3118
+ red.
3119
+
3120
+ << SCSS rules >>:
3121
+ .maui-warnings tt {
3122
+ color: inherit;
3123
+ }
3124
+
3125
+
3126
+ By medieval tradition, rubrics should be red. We'll use a
3127
+ slightly darker shade and go with what CSS calls [[crimson]]
3128
+ instead of [[red]].
3129
+
3130
+ << SCSS colours >>:
3131
+ $rubric-colour: crimson;
3132
+
3133
+ << SCSS rules >>:
3134
+ .maui-rubric {
3135
+ color: $rubric-colour;
3136
+ }
3137
+
3138
+
3139
+ Maui outputs warnings in unordered lists, but we don't actually
3140
+ want bullets in front of them.
3141
+
3142
+ << SCSS rules >>:
3143
+ ul.maui-warnings {
3144
+ padding-left: 0;
3145
+ > li {
3146
+ list-style: none;
3147
+ }
3148
+ }
3149
+
3150
+
3151
+ Let us now proceed to styling the code chunks themselves.
3152
+ First, the rules:
3153
+
3154
+ << SCSS colours >>:
3155
+ $chunk-rule-colour: #cccccc;
3156
+
3157
+ << SCSS dimensions >>:
3158
+ $chunk-body-indent: 20px;
3159
+ $chunk-rule-thickness: 2px;
3160
+ $chunk-rule-separation: 5px;
3161
+ $final-chunk-rule-length: 40px;
3162
+
3163
+ << SCSS rules >>:
3164
+ .maui-chunk-body {
3165
+ margin-left: $chunk-body-indent;
3166
+ border-left: $chunk-rule-thickness solid $chunk-rule-colour;
3167
+ padding-left: $chunk-rule-separation;
3168
+ }
3169
+
3170
+ .maui-initial-chunk>.maui-chunk-body:before {
3171
+ content: "";
3172
+ display: block;
3173
+ width: $chunk-body-indent + $chunk-rule-thickness;
3174
+ border-top: solid $chunk-rule-thickness $chunk-rule-colour;
3175
+ margin-left: -($chunk-body-indent + $chunk-rule-thickness +
3176
+ $chunk-rule-separation);
3177
+ }
3178
+
3179
+ .maui-final-chunk>.maui-chunk-body:after {
3180
+ content: "";
3181
+ display: block;
3182
+ margin-left:
3183
+ -($chunk-rule-thickness + $chunk-rule-separation);
3184
+ width: $final-chunk-rule-length;
3185
+ border-bottom:
3186
+ solid $chunk-rule-thickness $chunk-rule-colour;
3187
+ }
3188
+
3189
+
3190
+ A chunk's body shall have zero vertical margins; its containing
3191
+ [[maui-chunk]] will take care of vertical separation. This also
3192
+ applies to the block of warnings inside a chunk.
3193
+
3194
+ << SCSS dimensions >>:
3195
+ $paragraph-sep: 16px;
3196
+
3197
+ << SCSS rules >>:
3198
+
3199
+ .maui-chunk-body, .maui-chunk>.maui-warnings {
3200
+ margin-top: 0;
3201
+ margin-bottom: 0;
3202
+ }
3203
+
3204
+ .maui-chunk {
3205
+ margin-top: $paragraph-sep;
3206
+ margin-bottom: $paragraph-sep;
3207
+ }
3208
+
3209
+
3210
+ The cross-references following (final) chunks are smaller and in
3211
+ italic for reduced obtrusivity, and they align with the chunk's
3212
+ vertical rule but are deliberately slightly off from aligning
3213
+ with the code inside the chunk.
3214
+
3215
+ .maui-chunk-xref {
3216
+ font-size: small;
3217
+ font-style: italic;
3218
+ margin-left:
3219
+ $chunk-body-indent + $chunk-rule-thickness;
3220
+ }
3221
+
3222
+
3223
+ Finally, this rule tells pre-HTML5 browsers that [[section]] is
3224
+ [[div]]-like, not [[span]]-like:
3225
+
3226
+ << SCSS rules >>:
3227
+ /* Backwards compatibility with pre-HTML5 browsers */
3228
+ section {
3229
+ display: block;
3230
+ }
3231
+
3232
+
3233
+ == The skeletal composition of the library
3234
+
3235
+ << .file lib/mau/fabricator.rb >>:
3236
+ # encoding: UTF-8
3237
+
3238
+ require 'ostruct'
3239
+ require 'rbconfig'
3240
+ require 'set'
3241
+ require 'stringio'
3242
+
3243
+ << Outer definitions >>
3244
+
3245
+ module Fabricator
3246
+ << in [[Fabricator]] >>
3247
+ end
3248
+
3249
+ class << Fabricator
3250
+ << Other methods >>
3251
+ end
3252
+
3253
+
3254
+ == The command line interface
3255
+
3256
+ << .script bin/maui >>:
3257
+ #! /usr/bin/ruby -rubygems
3258
+ # encoding: UTF-8
3259
+
3260
+ require 'getoptlong'
3261
+ require 'mau/fabricator'
3262
+
3263
+ $0 = 'maui' # for [[GetoptLong]] error reporting
3264
+ << Parse command line >>
3265
+
3266
+ fabric = open $cmdline.fabric_filename, 'r' do |port|
3267
+ Fabricator.load_fabric port,
3268
+ chunk_size_limit: $cmdline.chunk_size_limit
3269
+ end
3270
+
3271
+ << Set up the [[writeout_plan]] >>
3272
+
3273
+ Fabricator.show_warnings fabric
3274
+
3275
+ << Execute the [[writeout_plan]] >>
3276
+
3277
+
3278
+ << Set up the [[writeout_plan]] >>:
3279
+ writeout_plan = {}
3280
+ << Plan to write out tangling results >>
3281
+ << Plan to write out specially generated files >>
3282
+
3283
+
3284
+ << Plan to write out tangling results >>:
3285
+ fabric.tangles.each_value do |results|
3286
+ writeout_plan[results.filename] =
3287
+ Fabricator.plan_to_write_out(results)
3288
+ end
3289
+
3290
+
3291
+ << Plan to write out specially generated files >>:
3292
+ [
3293
+ << Special out-writables >>
3294
+ ].each do |special|
3295
+ filename = File.basename($cmdline.fabric_filename).
3296
+ sub(/(\.fab)?$/i, special.suffix)
3297
+ if writeout_plan.has_key? filename then
3298
+ number = fabric.warnings.length + 1
3299
+ first_header = fabric.chunks_by_name[filename].
3300
+ headers.first
3301
+ warning = OpenStruct.new(
3302
+ loc: first_header.header_loc,
3303
+ message: "name clash with #{special.description}",
3304
+ number: number,
3305
+ inline: true,
3306
+ )
3307
+ fabric.warnings.push warning
3308
+ (first_header.warnings ||= []).push warning
3309
+ # For ordering purposes, we'll delete the old value before
3310
+ # adding the new one at the same key.
3311
+ writeout_plan.delete filename
3312
+ end
3313
+ writeout_plan[filename] = special.generator
3314
+ end
3315
+
3316
+
3317
+ << Special out-writables >>:
3318
+
3319
+ OpenStruct.new(
3320
+ suffix: '.html',
3321
+ description: 'HTML weaving',
3322
+ generator: proc do |filename|
3323
+ << Weave [[fabric]] (HTML) >>
3324
+ end,
3325
+ ),
3326
+
3327
+
3328
+ OpenStruct.new(
3329
+ suffix: '.ctxt',
3330
+ description: 'ctxt weaving',
3331
+ generator: proc do |filename|
3332
+ << Weave [[fabric]] (ctxt) >>
3333
+ end,
3334
+ ),
3335
+
3336
+
3337
+ << Execute the [[writeout_plan]] >>:
3338
+ exit_code = 0
3339
+ (ARGV.empty? ? writeout_plan.keys : ARGV.uniq).
3340
+ each do |filename|
3341
+ if thunk = writeout_plan[filename] then
3342
+ path = filename.split '/'
3343
+ (0 .. path.length - 2).each do |i|
3344
+ dirname = path[0 .. i].join '/'
3345
+ begin
3346
+ Dir.mkdir dirname
3347
+ puts "Created directory #{dirname}"
3348
+ rescue Errno::EEXIST
3349
+ end
3350
+ end
3351
+ thunk.call filename
3352
+ else
3353
+ $stderr.puts "maui: #{filename}: unknown output file"
3354
+ exit_code = 1
3355
+ end
3356
+ end
3357
+ exit exit_code
3358
+
3359
+
3360
+ == Rubygem metadata
3361
+
3362
+ << .file maui.gemspec >>:
3363
+ # This file is tangled from [[maui.fab]].
3364
+ # Please do not edit directly.
3365
+
3366
+ Gem::Specification.new do |s|
3367
+ s.name = 'maui'
3368
+ s.version = '<< VERSION >>'
3369
+ s.date = '2014-09-23'
3370
+ s.homepage = 'https://github.com/digwuren/maui'
3371
+ s.summary = 'A wiki-style literate programming engine'
3372
+ s.author = 'Andres Soolo'
3373
+ s.email = 'dig@mirky.net'
3374
+ s.files = File.read('Manifest.txt').split(/\n/)
3375
+ s.executables << 'maui'
3376
+ s.license = 'GPL-3'
3377
+ s.description = <<EOD
3378
+ Fabricator is a literate programming engine with wiki-like
3379
+ notation. Mau is a PIM-oriented wiki engine built around
3380
+ Fabricator. This gem contains Maui, the Mau Independent
3381
+ Fabricator, allowing Fabricator to be used via a command line
3382
+ interface or via the Ruby API without a need to employ a full
3383
+ installation of Mau.
3384
+ EOD
3385
+ s.has_rdoc = false
3386
+ end
3387
+
3388
+ << .file Manifest.txt >>:
3389
+ GPL-3
3390
+ Makefile
3391
+ Manifest.txt
3392
+ README.fab
3393
+ README.html
3394
+ bin/maui
3395
+ lib/mau/fabricator.rb
3396
+ maui.fab
3397
+ maui.gemspec
3398
+
3399
+
3400
+ * The command line parser.
3401
+
3402
+ << Parse command line >>:
3403
+ begin
3404
+ << Parse command line options >>
3405
+ << Parse command line arguments >>
3406
+ rescue GetoptLong::Error => e
3407
+ # no need to display; it has already been reported
3408
+ exit 1
3409
+ end
3410
+
3411
+ << Parse command line options >>:
3412
+ $cmdline = OpenStruct.new
3413
+ << Initialise [[$cmdline]] .dense >>
3414
+
3415
+ GetoptLong.new(
3416
+ << Option declarations >>
3417
+ ).each do |opt, arg|
3418
+ case opt
3419
+ << Command line option handlers >>
3420
+ else
3421
+ raise 'assertion failed'
3422
+ end
3423
+ end
3424
+
3425
+
3426
+ << Option summary >>:
3427
+ --output-width=N
3428
+ Word-wrap the woven ctxt at this width.
3429
+
3430
+ << Option declarations >>:
3431
+ ['--output-width', GetoptLong::REQUIRED_ARGUMENT],
3432
+
3433
+ << Initialise [[$cmdline]] >>:
3434
+ $cmdline.output_width = 80
3435
+
3436
+ << Command line option handlers >>:
3437
+ when '--output-width' then
3438
+ unless arg =~ /\A\d+\Z/ then
3439
+ $stderr.puts "maui: --output-width requires a number"
3440
+ exit 1
3441
+ end
3442
+ $cmdline.output_width = arg.to_i
3443
+
3444
+
3445
+ Chunks longer than this many lines will be dubbed 'long' by a
3446
+ warning. If longer than twice this many lines, 'very long'.
3447
+ Zero disables this check.
3448
+
3449
+ << Option summary >>:
3450
+ --chunk-size-limit=LINE-COUNT
3451
+ Consider chunks longer than this many lines warnably long.
3452
+ Chunks longer than twice this many lines will be
3453
+ considered warnably very long.
3454
+
3455
+ << Option declarations >>:
3456
+ ['--chunk-size-limit', GetoptLong::REQUIRED_ARGUMENT],
3457
+
3458
+ << Initialise [[$cmdline]] >>:
3459
+ $cmdline.chunk_size_limit = 24
3460
+
3461
+ << Command line option handlers >>:
3462
+ when '--chunk-size-limit' then
3463
+ unless arg =~ /\A\d+\Z/ then
3464
+ $stderr.puts "maui: --chunk-size-limit " +
3465
+ "requires a number"
3466
+ exit 1
3467
+ end
3468
+ arg = arg.to_i
3469
+ arg = nil if arg <= 0
3470
+ $cmdline.chunk_size_limit = arg
3471
+
3472
+
3473
+ For HTML output, the [[--link-css]] option permits the user to
3474
+ specify one or more stylesheets that should be applied to the
3475
+ result.
3476
+
3477
+ << Option summary >>:
3478
+ --link-css=URL
3479
+ Specify a stylesheet to be applied to the woven HTML.
3480
+ Availability of the target CSS to the browser and the
3481
+ relativity of the link are user's responsibility. If used
3482
+ multiple times, all these links will be used, and their
3483
+ order is preserved.
3484
+
3485
+ Usage of this option suppresses including the default,
3486
+ built-in stylesheet in the output.
3487
+
3488
+ << Option declarations >>:
3489
+ ['--link-css', GetoptLong::REQUIRED_ARGUMENT],
3490
+
3491
+ << Initialise [[$cmdline]] >>:
3492
+ $cmdline.link_css = []
3493
+
3494
+ << Command line option handlers >>:
3495
+ when '--link-css' then
3496
+ $cmdline.link_css.push arg
3497
+
3498
+
3499
+ << Parse command line arguments >>:
3500
+ if ARGV.empty? then
3501
+ $stderr.puts "maui: no fabric filename given"
3502
+ exit 1
3503
+ end
3504
+ $cmdline.fabric_filename = ARGV.shift
3505
+
3506
+
3507
+ << Command line option handlers >>:
3508
+ when '--unicode-boxes' then
3509
+ $cmdline.pseudographics =
3510
+ Fabricator::UNICODE_PSEUDOGRAPHICS
3511
+
3512
+ << Option declarations >>:
3513
+ ['--unicode-boxes', GetoptLong::NO_ARGUMENT],
3514
+
3515
+
3516
+ << Command line option handlers >>:
3517
+ when '--ascii-boxes' then
3518
+ $cmdline.pseudographics = Fabricator::ASCII_PSEUDOGRAPHICS
3519
+
3520
+ << Option declarations >>:
3521
+ ['--ascii-boxes', GetoptLong::NO_ARGUMENT],
3522
+
3523
+
3524
+ Finally, the GNU Coding Standards recommend implementing
3525
+ [[--help]] and [[--version]].
3526
+
3527
+ << Option summary >>:
3528
+ --help
3529
+ Print this usage.
3530
+
3531
+ << Option declarations >>:
3532
+ ['--help', GetoptLong::NO_ARGUMENT],
3533
+
3534
+ << Command line option handlers >>:
3535
+ when '--help' then
3536
+ puts "<< .clearindent Usage help >>"
3537
+ puts
3538
+ exit 0
3539
+
3540
+ << Usage help >>:
3541
+ Usage: maui [options] fabric-file
3542
+
3543
+ Process the given Mau fabric, tangle its files and weave its
3544
+ narrative into both HTML and coloured text.
3545
+
3546
+ << Option summary >>
3547
+
3548
+ Report bugs to: <dig@mirky.net>
3549
+
3550
+
3551
+ << Option summary >>:
3552
+ --version
3553
+ Show version data.
3554
+
3555
+ << Option declarations >>:
3556
+ ['--version', GetoptLong::NO_ARGUMENT],
3557
+
3558
+ << Command line option handlers >>:
3559
+ when '--version' then
3560
+ puts "<< .clearindent [[--version]] output >>"
3561
+ puts
3562
+ exit 0
3563
+
3564
+ << [[--version]] output >>:
3565
+ << IDENT >>
3566
+ Copyright (C) 2003-2014 Andres Soolo
3567
+ Copyright (C) 2013-2014 Knitten Development Ltd.
3568
+
3569
+ Licensed under GPLv3+: GNU GPL version 3 or later
3570
+ <http://gnu.org/licenses/gpl.html>
3571
+
3572
+ This is free software: you are free to change and
3573
+ redistribute it.
3574
+
3575
+ There is NO WARRANTY, to the extent permitted by law.
3576
+