UNPKG

11.8 kBJavaScriptView Raw
1var GitHubSlugger = require('github-slugger')
2var englishList = require('english-list')
3var escape = require('escape-html')
4var group = require('commonform-group-series')
5var has = require('has')
6var hash = require('commonform-hash')
7var predicate = require('commonform-predicate')
8var smartify = require('commonform-smartify')
9
10function renderParagraph (paragraph, offset, path, blanks, options) {
11 var html5 = options.html5
12 return (
13 '<p>' +
14 paragraph.content
15 .map(function (element, index) {
16 if (predicate.text(element)) {
17 return escape(element)
18 } else if (predicate.use(element)) {
19 return renderUse(element.use)
20 } else if (predicate.definition(element)) {
21 return (
22 (html5 ? '<dfn>' : '<span class="definition">') +
23 escape(element.definition) +
24 (html5 ? '</dfn>' : '</span>')
25 )
26 } else if (predicate.blank(element)) {
27 var elementPath = path.concat('content', offset + index)
28 var value = matchingValue(elementPath, blanks)
29 return (
30 '<span class="blank">' +
31 (value ? escape(value) : escape('[•]')) +
32 '</span>'
33 )
34 } else if (predicate.reference(element)) {
35 return renderReference(element.reference, options)
36 }
37 })
38 .join('') +
39 '</p>'
40 )
41}
42
43function renderUse (term) {
44 return (
45 '<span class="term">' +
46 escape(term) +
47 '</span>'
48 )
49}
50
51function renderReference (heading, options) {
52 if (options.ids) {
53 options.referenceSlugger.reset()
54 var slug = options.referenceSlugger.slug(heading)
55 return (
56 '<a class="reference" href="#' + slug + '">' +
57 escape(heading) +
58 '</a>'
59 )
60 } else {
61 return (
62 '<span class="reference">' +
63 escape(heading) +
64 '</span>'
65 )
66 }
67}
68
69function matchingValue (path, blanks) {
70 var length = blanks.length
71 for (var index = 0; index < length; index++) {
72 var blank = blanks[index]
73 if (equal(blank.blank, path)) {
74 return blank.value
75 }
76 }
77}
78
79function heading (depth, text, options) {
80 var id = options.ids
81 ? ' id="' + encodeURIComponent(
82 options.headingSlugger.slug(text)
83 ) + '"'
84 : ''
85 if (depth <= 6) {
86 return (
87 '<h' + depth + id + '>' +
88 escape(text) +
89 '</h' + depth + '>'
90 )
91 } else {
92 return (
93 '<span class="h' + depth + '"' + id + '>' +
94 escape(text) +
95 '</span>'
96 )
97 }
98}
99
100function renderSeries (depth, offset, path, series, blanks, options) {
101 var simple = options.lists && !series.content.some(containsAHeading)
102 var html5 = options.html5
103 if (simple) {
104 return (
105 '<ol>' +
106 series.content
107 .map(function (child, index) {
108 var childPath = path.concat('content', offset + index, 'form')
109 var classes = []
110 var component = predicate.component(child)
111 if (component) classes.push('component')
112 if (!component && child.form.conspicuous) {
113 classes.push('conspicuous')
114 }
115 return (
116 (
117 classes.length > 0
118 ? '<li class="' + classes.join(' ') + '">'
119 : '<li>'
120 ) +
121 (
122 component
123 ? (
124 renderComponent(
125 depth,
126 childPath,
127 child,
128 blanks,
129 options
130 )
131 )
132 : renderChild(depth, childPath, child.form, blanks, options)
133 ) +
134 '</li>'
135 )
136 })
137 .join('') +
138 '</ol>'
139 )
140 } else {
141 return series.content
142 .map(function (child, index) {
143 var childPath = path.concat('content', offset + index, 'form')
144 var classes = []
145 var component = predicate.component(child)
146 if (component) classes.push('component')
147 if (!component && child.form.conspicuous) {
148 classes.push('conspicuous')
149 }
150 if (!html5) classes.push('section')
151 return (
152 (
153 html5
154 ? classes.length > 0
155 ? '<section class="' + classes.join(' ') + '">'
156 : '<section>'
157 : '<div class="' + classes.join(' ') + '">'
158 ) +
159 ('heading' in child ? heading(depth, child.heading, options) : '') +
160 (
161 component
162 ? (
163 renderComponent(
164 depth,
165 childPath,
166 child,
167 blanks,
168 options
169 )
170 )
171 : renderChild(depth, childPath, child.form, blanks, options)
172 ) +
173 (html5 ? '</section>' : '</div>')
174 )
175 })
176 .join('')
177 }
178}
179
180function renderChild (depth, path, form, blanks, options) {
181 return (
182 renderAnnotations(path, options.annotations, options) +
183 renderForm(depth, path, form, blanks, options)
184 )
185}
186
187function renderForm (depth, path, form, blanks, options) {
188 var offset = 0
189 return group(form)
190 .map(function (group) {
191 var returned = group.type === 'series'
192 ? renderSeries(
193 depth + 1, offset, path, group, blanks, options
194 )
195 : renderParagraph(group, offset, path, blanks, options)
196 offset += group.content.length
197 return returned
198 })
199 .join('')
200}
201
202function renderComponent (depth, path, component, blanks, options) {
203 if (has(component, 'form')) {
204 return renderLoadedComponent(depth, path, component, blanks, options)
205 } else {
206 return renderComponentReference(depth, path, component, blanks, options)
207 }
208}
209
210function renderLoadedComponent (depth, path, component, blanks, options) {
211 var style = options.loadedComponentStyle
212 if (style === 'copy') {
213 return renderChild(depth, path, component.form, blanks, options)
214 } else if (style === 'reference') {
215 return renderLoadedComponentReference(depth, path, component, blanks, options)
216 } else if (style === 'both') {
217 return renderLoadedComponentBoth(depth, path, component, blanks, options)
218 } else {
219 throw new Error('Uknown loaded component display style: ' + style)
220 }
221}
222
223function renderLoadedComponentReference (depth, path, component, blanks, options) {
224 var returned = '<p>' + options.incorporateComponentText
225 returned += ' '
226 var url = component.reference.component + '/' + component.reference.version
227 returned += '<a href="' + url + '">'
228 var meta = component.component
229 returned += meta.publisher
230 returned += ' '
231 returned += meta.name
232 returned += ' '
233 returned += meta.version
234 returned += '</a>'
235 var substitutions = component.reference.substitutions
236 var hasSubstitutions = (
237 Object.keys(substitutions.terms).length > 0 ||
238 Object.keys(substitutions.headings).length > 0 ||
239 Object.keys(substitutions.blanks).length > 0
240 )
241 if (hasSubstitutions) {
242 returned += ' substituting:</p>'
243 returned += renderSubstitutions(component.reference.substitutions, options)
244 } else {
245 returned += '.</p>'
246 }
247 return returned
248}
249
250function renderLoadedComponentBoth (depth, path, component, blanks, options) {
251 var returned = renderLoadedComponentReference(depth, path, component, blanks, options)
252 returned += '<p>'
253 returned += options.quoteComponentText
254 returned += '</p>'
255 returned += renderAnnotations(path, options.annotations, options)
256 returned += '<blockquote>'
257 returned += renderForm(depth, path, component.form, blanks, options)
258 returned += '</blockquote>'
259 return returned
260}
261
262function renderComponentReference (depth, path, component, blanks, options) {
263 var url = component.component + '/' + component.version
264 var substitutions = component.substitutions
265 var hasSubstitutions = (
266 Object.keys(substitutions.terms).length > 0 ||
267 Object.keys(substitutions.headings).length > 0 ||
268 Object.keys(substitutions.blanks).length > 0
269 )
270 var returned = '<p>' + (options.incorporate || 'Incorporate') + ' <a href="' + url + '">' + url + '</a>'
271 if (hasSubstitutions) {
272 returned += ' substituting:</p>'
273 returned += renderSubstitutions(substitutions, options)
274 } else {
275 returned += '.</p>'
276 }
277 return returned
278}
279
280function renderSubstitutions (substitutions, options) {
281 return '<ul>' +
282 Object.keys(substitutions.terms).sort().map(function (from) {
283 var to = substitutions.terms[from]
284 return '<li>the term ' + quote(to) + ' for the term ' + quote(from) + '</li>'
285 }).join('') +
286 Object.keys(substitutions.headings).sort().map(function (from) {
287 var to = substitutions.headings[from]
288 return '<li>references to ' + quote(to) + ' for references to ' + quote(from) + '</li>'
289 }).join('') +
290 Object.keys(substitutions.blanks)
291 .sort(function (a, b) { return parseInt(a) - parseInt(b) })
292 .map(function (number) {
293 var value = substitutions.blanks[number]
294 return '<li>' + quote(value) + ' for blank ' + number + '</li>'
295 }).join('') +
296 '</ul>'
297
298 function quote (string) {
299 if (options.smartify) return '“' + string + '”'
300 else return '"' + string + '"'
301 }
302}
303
304function renderAnnotations (path, annotations, options) {
305 var tag = options.html5 ? 'aside' : 'div'
306 return annotations
307 .filter(function (annotation) {
308 return equal(annotation.path.slice(0, -2), path)
309 })
310 .map(function (annotation) {
311 var classNames = ['annotation', annotation.level]
312 var paragraph = '<p>' + escape(annotation.message) + '</p>'
313 return [
314 '<' + tag + ' class="' + classNames.sort().join(' ') + '">',
315 paragraph,
316 '</' + tag + '>'
317 ].join('')
318 })
319 .join('')
320}
321
322module.exports = function commonformHTML (form, blanks, options) {
323 blanks = blanks || []
324 options = options || {}
325 var html5 = options.html5
326 var title = options.title
327 var version = options.version
328 var depth = options.depth || 0
329 var classNames = options.classNames || []
330 if (!options.quoteComponentText) {
331 options.quoteComponentText = 'Quoting for convenience, with any conflicts resolved in favor of the standard:'
332 }
333 if (!options.incorporateComponentText) {
334 options.incorporateComponentText = 'Incorporate'
335 }
336 options.annotations = options.annotations || []
337 if (options.ids) {
338 options.headingSlugger = new GitHubSlugger()
339 options.referenceSlugger = new GitHubSlugger()
340 }
341 if (!html5) classNames.push('article')
342 if (form.conspicuous) classNames.push('conspicuous')
343 classNames.sort()
344 if (title) depth++
345 return (
346 (
347 html5
348 ? classNames.length === 0
349 ? '<article>'
350 : '<article class="' + classNames.join(' ') + '">'
351 : '<div class="' + classNames.join(' ') + '">'
352 ) +
353 (title ? ('<h1>' + escape(title) + '</h1>') : '') +
354 (version ? ('<p class="version">' + escape(version) + '</p>') : '') +
355 (
356 options.hash
357 ? ('<p class="hash"><code>' + hash(form) + '</code></p>')
358 : ''
359 ) +
360 renderAnnotations([], options.annotations, options) +
361 renderForm(depth, [], options.smartify ? smartify(form) : form, blanks, options) +
362 (html5 ? '</article>' : '</div>')
363 )
364}
365
366function equal (a, b) {
367 return (
368 Array.isArray(a) &&
369 Array.isArray(b) &&
370 a.length === b.length &&
371 a.every(function (_, index) {
372 return a[index] === b[index]
373 })
374 )
375}
376
377function containsAHeading (child) {
378 return (
379 has(child, 'heading') ||
380 (
381 has(child, 'form') &&
382 child.form.content.some(function (element) {
383 return (
384 has(element, 'form') &&
385 containsAHeading(element)
386 )
387 })
388 )
389 )
390}