1 | var GitHubSlugger = require('github-slugger')
|
2 | var englishList = require('english-list')
|
3 | var escape = require('escape-html')
|
4 | var group = require('commonform-group-series')
|
5 | var has = require('has')
|
6 | var hash = require('commonform-hash')
|
7 | var predicate = require('commonform-predicate')
|
8 | var smartify = require('commonform-smartify')
|
9 |
|
10 | function 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 |
|
43 | function renderUse (term) {
|
44 | return (
|
45 | '<span class="term">' +
|
46 | escape(term) +
|
47 | '</span>'
|
48 | )
|
49 | }
|
50 |
|
51 | function 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 |
|
69 | function 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 |
|
79 | function 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 |
|
100 | function 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 |
|
180 | function renderChild (depth, path, form, blanks, options) {
|
181 | return (
|
182 | renderAnnotations(path, options.annotations, options) +
|
183 | renderForm(depth, path, form, blanks, options)
|
184 | )
|
185 | }
|
186 |
|
187 | function 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 |
|
202 | function 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 |
|
210 | function 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 |
|
223 | function 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 |
|
250 | function 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 |
|
262 | function 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 |
|
280 | function 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 |
|
304 | function 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 |
|
322 | module.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 |
|
366 | function 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 |
|
377 | function 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 | }
|