maui 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+