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.
- checksums.yaml +7 -0
- data/GPL-3 +674 -0
- data/Makefile +17 -0
- data/Manifest.txt +9 -0
- data/README.fab +222 -0
- data/README.html +296 -0
- data/bin/maui +200 -0
- data/lib/mau/fabricator.rb +2071 -0
- data/maui.fab +3576 -0
- data/maui.gemspec +24 -0
- metadata +59 -0
data/maui.fab
ADDED
@@ -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 "«"
|
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 "»:"
|
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 "«"
|
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 "»"
|
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 ' '
|
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
|
+
'&' => '&',
|
3025
|
+
'<' => '<',
|
3026
|
+
'>' => '>',
|
3027
|
+
'"' => '"',
|
3028
|
+
"'" => ''',
|
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
|
+
|