web-documentation 1.0.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.
@@ -0,0 +1,431 @@
1
+ // #!/usr/bin/env babel-node
2
+ // -*- coding: utf-8 -*-
3
+ /** @module web-documentation */
4
+ 'use strict'
5
+ /* !
6
+ region header
7
+ [Project page](https://github.com/web-documentation)
8
+
9
+ Copyright Torben Sickert (info["~at~"]torben.website) 16.12.2012
10
+
11
+ License
12
+ -------
13
+
14
+ This library written by Torben Sickert stand under a creative commons
15
+ naming 3.0 unported license.
16
+ See https://creativecommons.org/licenses/by/3.0/deed.de
17
+ endregion
18
+ */
19
+ // region imports
20
+ import {
21
+ camelCaseToDelimited,
22
+ createDomNodes,
23
+ extend,
24
+ format,
25
+ getAll,
26
+ getParents,
27
+ getText,
28
+ globalContext,
29
+ Logger,
30
+ Mapping,
31
+ NOOP,
32
+ wrap
33
+ } from 'clientnode'
34
+ import {func, object} from 'clientnode/property-types'
35
+ import {property} from 'web-component-wrapper/decorator'
36
+ import {WebComponentAPI} from 'web-component-wrapper/type'
37
+ import {Web} from 'web-component-wrapper/Web'
38
+ import {api as websiteUtilitiesAPI} from 'website-utilities'
39
+ import {api as webInternationalizationAPI} from 'web-internationalization'
40
+
41
+ import {DefaultOptions, Options} from './type'
42
+ // endregion
43
+ export const log = new Logger({name: 'web-documentation'})
44
+ // region plugins/classes
45
+ /**
46
+ * This plugin holds all needed methods to extend a whole documentation site.
47
+ * @property _defaultOptions - Options extended by the options given to the
48
+ * initializer method.
49
+ * @property _defaultOptions.selectors - Object with a mapping of needed dom
50
+ * node descriptions to their corresponding selectors.
51
+ * @property _defaultOptions.showExample - Options object to configure code
52
+ * example representation.
53
+ * @property _defaultOptions.showExample.pattern - Regular expression to
54
+ * introduce a code example section.
55
+ * @property _defaultOptions.showExample.domNodeName - Dom node name to
56
+ * indicate a declarative example section.
57
+ * @property _defaultOptions.showExample.htmlWrapper - HTML example wrapper.
58
+ * @property _defaultOptions.section - Configuration object for section
59
+ * switches between the main page and legal notes descriptions.
60
+ * @property options - Finally configured given options.
61
+ */
62
+ export class WebDocumentation<
63
+ TElement = HTMLElement,
64
+ ExternalProperties extends Mapping<unknown> = Mapping<unknown>,
65
+ InternalProperties extends Mapping<unknown> = Mapping<unknown>
66
+ > extends Web<TElement, ExternalProperties, InternalProperties> {
67
+ static content = `
68
+ <website-utilities
69
+ options="{sectionNames: ['home', 'about-this-website']}"
70
+ >
71
+ <web-internationalization>
72
+ <slot>Please provide a template to transclude.</slot>
73
+ </web-internationalization>
74
+ </website-utilities>
75
+ `
76
+
77
+ static _name = 'WebDocumentation'
78
+
79
+ static _defaultOptions: DefaultOptions = {
80
+ selectors: {
81
+ aboutThisWebsiteLink: 'a[href="#about-this-website"]',
82
+ aboutThisWebsiteSection: '.section__about-this-website',
83
+
84
+ codeWrapper: 'pre',
85
+ code: 'code',
86
+
87
+ headlines:
88
+ '.section__main h1, .section__main h2, ' +
89
+ '.section__main h3, .section__main h4, ' +
90
+ '.section__main h5, .section__main h6',
91
+ tableOfContent: '.doc-toc',
92
+ tableOfContentLinks: '.doc-toc ul li a[href^="#"]'
93
+ },
94
+
95
+ showExample: {
96
+ domNodeName: '#comment',
97
+ htmlWrapper: `
98
+ <div class="show-example-wrapper">
99
+ <h3>
100
+ Example:
101
+ <!--deDE:Beispiel:-->
102
+ <!--frFR:Exemple:-->
103
+ </h3>
104
+ {1}
105
+ </div>
106
+ `,
107
+ pattern: '^ *showExample(: *([^ ]+))? *$'
108
+ }
109
+ }
110
+
111
+ readonly self = WebDocumentation
112
+ // region api properties
113
+ @property({type: object})
114
+ options = {} as Options
115
+ // endregion
116
+ // region domNodes
117
+ aboutThisWebsiteLinkDomNodes: NodeListOf<HTMLElement> | null = null
118
+ aboutThisWebsiteSectionDomNode: HTMLDivElement | null = null
119
+
120
+ codeDomNodes: NodeListOf<HTMLElement> | null = null
121
+
122
+ headlineDomNodes: NodeListOf<HTMLElement> | null = null
123
+ tableOfContentDomNode: HTMLElement | null = null
124
+ tableOfContentLinkDomNodes: NodeListOf<HTMLAnchorElement> | null = null
125
+ // endregion
126
+ // region public
127
+ /// region live-cycle
128
+ /**
129
+ * Defines dynamic getter and setter interface and resolves configuration
130
+ * object. Initializes the map implementation.
131
+ */
132
+ constructor() {
133
+ super()
134
+ /*
135
+ Babels property declaration transformation overwrites defined
136
+ properties at the end of an implicit constructor. So we have to
137
+ redefined them as long as we want to declare expected component
138
+ interface properties to enable static type checks.
139
+ */
140
+ this.defineGetterAndSetterInterface()
141
+ }
142
+ /**
143
+ * Triggered when ever a given attribute has changed and triggers to update
144
+ * configured dom content.
145
+ * @param name - Attribute name which was updates.
146
+ * @param newValue - New updated value.
147
+ */
148
+ onUpdateAttribute(name: string, newValue: string) {
149
+ super.onUpdateAttribute(name, newValue)
150
+
151
+ if (name === 'options')
152
+ this.options = extend<Options>(
153
+ true,
154
+ {},
155
+ this.self._defaultOptions,
156
+ this.options
157
+ )
158
+ }
159
+ /**
160
+ * Updates controlled dom elements.
161
+ * @param reason - Why an update has been triggered.
162
+ */
163
+ async render(reason?: string): Promise<void> {
164
+ await super.render(reason)
165
+
166
+ if (Object.keys(this.options).length === 0)
167
+ this.onUpdateAttribute('options', '{}')
168
+
169
+ this.grabDomNodes()
170
+
171
+ /*
172
+ NOTE: We have to render examples first to avoid having dots in
173
+ example code.
174
+ */
175
+ this._showExamples()
176
+
177
+ // TODO we may need to delay internationalization until this has been
178
+ // finished rendering
179
+
180
+ this._makeCodeEllipsis()
181
+
182
+ this._generateTableOfContentsLinks()
183
+ }
184
+ /// endregion
185
+ grabDomNodes(): void {
186
+ this.aboutThisWebsiteLinkDomNodes = this.root.querySelectorAll(
187
+ this.options.selectors.aboutThisWebsiteLink
188
+ )
189
+ this.aboutThisWebsiteSectionDomNode = this.root.querySelector(
190
+ this.options.selectors.aboutThisWebsiteSection
191
+ )
192
+
193
+ this.codeDomNodes =
194
+ this.root.querySelectorAll(this.options.selectors.code)
195
+
196
+ this.headlineDomNodes =
197
+ this.root.querySelectorAll(this.options.selectors.headlines)
198
+ this.tableOfContentDomNode =
199
+ this.root.querySelector(this.options.selectors.tableOfContent)
200
+ }
201
+ // endregion
202
+ // region protected methods
203
+ /// region event handler
204
+ /// endregion
205
+ /**
206
+ * Generates a table of contents via creating links referring to headlines.
207
+ */
208
+ _generateTableOfContentsLinks(): void {
209
+ if (!this.tableOfContentDomNode)
210
+ return
211
+
212
+ let listItems = '<ul>'
213
+ let level = 0
214
+ let firstLevel = 0
215
+ let first = true
216
+ for (const domNode of this.headlineDomNodes ?? []) {
217
+ if (getParents(domNode).some((domNode: Node) =>
218
+ (domNode as Partial<Element>).classList?.contains(
219
+ 'show-example-wrapper'
220
+ )
221
+ ))
222
+ return
223
+
224
+ const newLevel: number =
225
+ parseInt(domNode.nodeName.replace(/\D/g, ''))
226
+
227
+ if (first)
228
+ firstLevel = newLevel
229
+
230
+ if (newLevel > level)
231
+ listItems += '<ul>'
232
+ else if (newLevel < level)
233
+ listItems += '</ul>'
234
+
235
+ listItems += `
236
+ <li>
237
+ <a href="#${domNode.getAttribute('id') ?? 'unknown'}">
238
+ ${domNode.innerText}
239
+ </a>
240
+ </li>
241
+ `
242
+
243
+ level = newLevel
244
+ first = false
245
+ }
246
+ // Close remaining inner lists.
247
+ while (level < firstLevel) {
248
+ listItems += '</ul>'
249
+ level += 1
250
+ }
251
+
252
+ listItems += '</ul>'
253
+
254
+ this.tableOfContentDomNode.append(listItems)
255
+
256
+ this.tableOfContentLinkDomNodes =
257
+ this.tableOfContentDomNode.querySelectorAll<HTMLAnchorElement>('a')
258
+
259
+ this.tableOfContentDomNode.style.display = 'initial'
260
+ }
261
+ /**
262
+ * This method makes dotes after code lines which are too long. This
263
+ * prevents line wrapping.
264
+ */
265
+ _makeCodeEllipsis(): void {
266
+ const lengthLimit = 89 // 79
267
+
268
+ for (const domNode of this.codeDomNodes ?? []) {
269
+ let newContent = ''
270
+ const codeLines: Array<string> = domNode.innerHTML.split('\n')
271
+
272
+ let subIndex = 0
273
+ for (const value of codeLines) {
274
+ /*
275
+ NOTE: Wrap a div object to grantee that $ will accept the
276
+ input.
277
+ */
278
+ const excess: number =
279
+ getText(createDomNodes(`<div>${value}</div>`)).length -
280
+ lengthLimit
281
+ if (excess > 0)
282
+ newContent += this._replaceExcessWithDots(value, excess)
283
+ else
284
+ newContent += value
285
+ if (subIndex + 1 !== codeLines.length)
286
+ newContent += '\n'
287
+
288
+ subIndex += 1
289
+ }
290
+
291
+ domNode.innerHTML = newContent
292
+ }
293
+ }
294
+ /**
295
+ * Replaces given html content with a shorter version trimmed by given
296
+ * amount of excess.
297
+ * @param content - String to trim.
298
+ * @param excess - Amount of excess.
299
+ * @returns Returns the trimmed content.
300
+ */
301
+ _replaceExcessWithDots(content: string, excess: number): string {
302
+ // Add space for ending dots.
303
+ excess += '...'.length
304
+ let newContent = ''
305
+ const contentDomNodes =
306
+ getAll(createDomNodes(`<wrapper>${content}</wrapper>`))
307
+ contentDomNodes.reverse()
308
+ for (const domNode of contentDomNodes) {
309
+ const wrapper = createDomNodes<HTMLElement>('<wrapper><wrapper>')
310
+ wrap(domNode, wrapper)
311
+
312
+ const textContent = domNode.textContent || ''
313
+
314
+ let contentSnippet = wrapper.innerHTML
315
+ if (!contentSnippet)
316
+ contentSnippet = textContent
317
+
318
+ if (excess)
319
+ if (textContent.length < excess) {
320
+ excess -= textContent.length
321
+ contentSnippet = ''
322
+ } else if (textContent.length >= excess) {
323
+ /*
324
+ NOTE: We have to ensure that no HTML tag will be
325
+ shortened: We work on "textContent" property only.
326
+ */
327
+ domNode.textContent =
328
+ textContent.substring(
329
+ 0, textContent.length - excess - 1
330
+ ) +
331
+ '...'
332
+
333
+ excess = 0
334
+
335
+ contentSnippet = wrapper.innerHTML
336
+ if (!contentSnippet)
337
+ contentSnippet = domNode.textContent
338
+ }
339
+ newContent = contentSnippet + newContent
340
+ }
341
+
342
+ return newContent
343
+ }
344
+ /**
345
+ * Shows marked example codes directly in browser.
346
+ */
347
+ _showExamples(): void {
348
+ for (const domNode of getAll(this.root))
349
+ if (domNode.nodeName === this.options.showExample.domNodeName) {
350
+ const match: null | RegExpMatchArray =
351
+ (domNode.textContent || '').match(
352
+ new RegExp(this.options.showExample.pattern)
353
+ )
354
+ const codeDomNode = domNode.nextSibling as HTMLElement | null
355
+ if (match && codeDomNode) {
356
+ const codeWrapper: HTMLElement | null =
357
+ codeDomNode.querySelector(
358
+ this.options.selectors.codeWrapper
359
+ )
360
+ let code = codeWrapper?.innerText
361
+
362
+ if (!code)
363
+ code = codeDomNode.innerText
364
+
365
+ try {
366
+ let domNode: HTMLElement | string = ''
367
+ if (match.length > 2 && match[2])
368
+ if (
369
+ ['javascript', 'javascripts', 'js']
370
+ .includes(match[2].toLowerCase())
371
+ ) {
372
+ domNode = (globalContext.document as Document)
373
+ .createElement('script')
374
+ domNode.setAttribute('type', 'text/javascript')
375
+ domNode.innerText = code
376
+ } else if ([
377
+ 'css', 'cascadingstylesheet',
378
+ 'cascadingstylesheets', 'stylesheet',
379
+ 'stylesheets', 'sheet', 'sheets', 'style',
380
+ 'styles'
381
+ ].includes(match[2].toLowerCase())) {
382
+ domNode = (globalContext.document as Document)
383
+ .createElement('style')
384
+ domNode.setAttribute('type', 'text/css')
385
+ domNode.innerText = code
386
+ } else if (match[2].toLowerCase() === 'hidden')
387
+ domNode = code
388
+ else
389
+ domNode = createDomNodes(format(
390
+ this.options.showExample.htmlWrapper,
391
+ code
392
+ ))
393
+ else
394
+ domNode = createDomNodes(format(
395
+ this.options.showExample.htmlWrapper,
396
+ code
397
+ ))
398
+
399
+ codeDomNode.after(domNode)
400
+ } catch (error) {
401
+ log.critical(
402
+ `Error while integrating code "${code}":`,
403
+ String(error)
404
+ )
405
+ }
406
+ }
407
+ }
408
+
409
+
410
+ this.onExamplesLoaded.call(this)
411
+ }
412
+ // endregion
413
+ }
414
+ // endregion
415
+ export const api: WebComponentAPI<
416
+ HTMLElement, Mapping<unknown>, Mapping<unknown>, typeof Web
417
+ > = {
418
+ component: WebDocumentation,
419
+ register: (
420
+ tagName: string = camelCaseToDelimited(WebDocumentation._name)
421
+ ) => {
422
+ websiteUtilitiesAPI.register()
423
+ webInternationalizationAPI.register()
424
+
425
+ customElements.define(tagName, WebDocumentation)
426
+ }
427
+ }
428
+ export default WebDocumentation
429
+
430
+ if ((globalContext as Mapping<boolean>).AUTO_DEFINE_WEB_DOCUMENTATION)
431
+ api.register()
@@ -0,0 +1,31 @@
1
+ import {Marked} from 'marked'
2
+ import {gfmHeadingId} from 'marked-gfm-heading-id'
3
+ import {markedHighlight} from 'marked-highlight'
4
+ import highlightJSModule from 'highlight.js'
5
+ import {markedXhtml} from 'marked-xhtml'
6
+
7
+ const {getLanguage, highlight} = highlightJSModule
8
+
9
+ const marked = new Marked(
10
+ markedHighlight({
11
+ /*
12
+ A string to prefix the className in a <code> block. Useful for
13
+ syntax highlighting.
14
+ */
15
+ langPrefix: 'language-',
16
+ highlight: (code, lang, info) =>
17
+ highlight(
18
+ code, {language: getLanguage(lang) ? lang : 'plaintext'}
19
+ ).value
20
+ })
21
+ )
22
+
23
+ export default (options) => {
24
+ marked.setOptions(options)
25
+ // Include an id attribute when emitting headings (h1, h2, h3, etc).
26
+ marked.use(gfmHeadingId({prefix: 'doc-'}))
27
+ // Favors self-closing xhtml tags.
28
+ marked.use(markedXhtml())
29
+
30
+ return marked.parse
31
+ }
package/source/test.ts ADDED
@@ -0,0 +1,35 @@
1
+ // #!/usr/bin/env babel-node
2
+ // -*- coding: utf-8 -*-
3
+ 'use strict'
4
+ /* !
5
+ region header
6
+ Copyright Torben Sickert (info["~at~"]torben.website) 16.12.2012
7
+
8
+ License
9
+ -------
10
+
11
+ This library written by Torben Sickert stand under a creative commons
12
+ naming 3.0 unported license.
13
+ See https://creativecommons.org/licenses/by/3.0/deed.de
14
+ endregion
15
+ */
16
+ // region imports
17
+ import {beforeAll, describe, expect, test} from '@jest/globals'
18
+ import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter'
19
+
20
+ import WebDocumentation, {api} from './index'
21
+ // endregion
22
+ describe('WebDocumentation', () => {
23
+ let root: WebDocumentation
24
+
25
+ beforeAll(() => {
26
+ api.register()
27
+ root = document.createElement('web-documentation') as WebDocumentation
28
+ document.body.appendChild(root)
29
+ })
30
+ // region tests
31
+ test('should be defined', () => {
32
+ expect(root).toBeDefined()
33
+ })
34
+ // endregion
35
+ })
package/source/type.ts ADDED
@@ -0,0 +1,39 @@
1
+ // -*- coding: utf-8 -*-
2
+ /** @module type */
3
+ 'use strict'
4
+ /* !
5
+ region header
6
+ [Project page](https://torben.website/website-utilities)
7
+
8
+ Copyright Torben Sickert (info["~at~"]torben.website) 16.12.2012
9
+
10
+ License
11
+ -------
12
+
13
+ This library written by Torben Sickert stand under a creative commons
14
+ naming 3.0 unported license.
15
+ See https://creativecommons.org/licenses/by/3.0/deed.de
16
+ endregion
17
+ */
18
+ // region exports
19
+ export interface DefaultOptions {
20
+ selectors: {
21
+ aboutThisWebsiteLink: string
22
+ aboutThisWebsiteSection: string
23
+
24
+ codeWrapper: string
25
+ code: string
26
+
27
+ headlines: string
28
+ tableOfContent: string
29
+ tableOfContentLinks: string
30
+ }
31
+
32
+ showExample: {
33
+ domNodeName: string
34
+ htmlWrapper: string
35
+ pattern: string
36
+ }
37
+ }
38
+ export type Options = DefaultOptions
39
+ // endregion