@jseeio/jsee 0.3.4 → 0.3.7

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.
package/src/cli.js ADDED
@@ -0,0 +1,786 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const os = require('os')
4
+ const crypto = require('crypto')
5
+
6
+ const minimist = require('minimist')
7
+ const jsdoc2md = require('jsdoc-to-markdown')
8
+ const showdown = require('showdown')
9
+ const showdownKatex = require('showdown-katex')
10
+ const converter = new showdown.Converter({
11
+ extensions: [
12
+ showdownKatex({
13
+ throwOnError: true,
14
+ displayMode: true,
15
+ errorColor: '#1500ff',
16
+ output: 'mathml'
17
+ }),
18
+ ],
19
+ tables: true
20
+ })
21
+ showdown.setFlavor('github')
22
+
23
+ // left padding of multiple lines
24
+ function pad (str, len, start=0) {
25
+ return str.split('\n').map((s, i) => i >= start ? ' '.repeat(len) + s : s).join('\n')
26
+ }
27
+
28
+ function depad (str, len) {
29
+ return str.split('\n').map(s => s.slice(len)).join('\n')
30
+ }
31
+
32
+ // Adding async here breaks express. TODO: investigate
33
+ async function gen (pargv, returnHtml=false) {
34
+ pargv = pargv.slice(2) // Remove the first two elements (node and script path)
35
+
36
+ // print all folders (file location, cwd, etc.)
37
+ console.log('Current working directory:', process.cwd())
38
+ console.log('Script location:', __dirname)
39
+ console.log('Script file:', __filename)
40
+ console.log('Require', require.main.filename)
41
+
42
+ const argvAlias ={
43
+ inputs: 'i',
44
+ outputs: 'o',
45
+ description: 'd',
46
+ ga: 'g',
47
+ port: 'p',
48
+ version: 'v',
49
+ fetch: 'f',
50
+ execute: 'e'
51
+ }
52
+ const argvDefault = {
53
+ execute: false, // execute the model code on the server
54
+ fetch: false,
55
+ inputs: '',
56
+ port: 3000,
57
+ version: 'latest'
58
+ }
59
+
60
+ // Parse arguments using minimist
61
+ let argv = minimist(pargv, {
62
+ alias: argvAlias,
63
+ default: argvDefault,
64
+ })
65
+
66
+ // Set argv.inputs to the first non-option argument if it exists
67
+ if (argv._.length > 0) {
68
+ argv.inputs = argv._[0]
69
+ }
70
+
71
+ console.log('Initial argv:', argv)
72
+
73
+ let cwd = process.cwd()
74
+ let inputs = argv.inputs
75
+ let outputs = argv.outputs
76
+ let description = argv.description
77
+ let version = argv.version
78
+ let ga = argv.ga
79
+ let schema
80
+ let descriptionTxt = ''
81
+ let descriptionHtml = ''
82
+ let jsdocMarkdown = ''
83
+ let modelFuncs = {}
84
+
85
+ // if inputs is a string with js file names, split it into an array
86
+ if (typeof inputs === 'string') {
87
+ if (inputs.includes('.js')) {
88
+ inputs = inputs.split(',')
89
+ }
90
+ }
91
+
92
+ // if outputs is a string with js file names, split it into an array
93
+ if (typeof outputs === 'string') {
94
+ outputs = outputs.split(',')
95
+ }
96
+
97
+ // Check for schema.json in the current working directory
98
+ if (inputs.length === 0 && fs.existsSync(path.join(cwd, 'schema.json'))) {
99
+ console.log('Using schema.json from the current working directory')
100
+ inputs = ['schema.json']
101
+ }
102
+
103
+ if (inputs.length === 0) {
104
+ console.error('No inputs provided')
105
+ process.exit(1)
106
+ } else if ((inputs.length === 1) && (inputs[0].includes('.json'))) {
107
+ // Input is json schema
108
+ // Curren working directory if not provided
109
+ // schema = require(path.join(cwd, inputs[0]))
110
+ // switch to fs.readFileSync to reload the schema if it changes
111
+ schema = JSON.parse(fs.readFileSync(path.join(cwd, inputs[0]), 'utf8'))
112
+ } else {
113
+ // Array of js files
114
+ // Generate schema
115
+ let jsdocData = jsdoc2md.getTemplateDataSync({ files: inputs.map(f => path.join(cwd, f)) })
116
+ schema = genSchema(jsdocData)
117
+ // jsdocMarkdown = jsdoc2md.renderSync({
118
+ // data: jsdocData,
119
+ // 'param-list-format': 'list',
120
+ // })
121
+ }
122
+
123
+ // Iterate over schema inputs and update aliases
124
+ if (schema.inputs) {
125
+ schema.inputs.forEach(inp => {
126
+ if (inp.name) {
127
+ if (inp.alias) {
128
+ argvAlias[inp.name] = inp.alias
129
+ }
130
+ if (inp.default) {
131
+ argvDefault[inp.name] = inp.default
132
+ }
133
+ }
134
+ })
135
+ }
136
+
137
+ // Update argv with the new aliases and defaults
138
+ argv = minimist(pargv, {
139
+ alias: argvAlias,
140
+ default: argvDefault,
141
+ })
142
+
143
+ console.log('Updated argv', argv)
144
+
145
+ // Initially in argv.fetch branch
146
+ // Check if schema has model, convert to array if needed
147
+ if (!schema.model) {
148
+ console.error('No model found in schema')
149
+ process.exit(1)
150
+ }
151
+ if (!Array.isArray(schema.model)) {
152
+ schema.model = [schema.model]
153
+ }
154
+ if (argv.execute) {
155
+ schema.model.forEach(m => {
156
+ console.log('Executing model on the server:', m.name)
157
+ modelFuncs[m.name] = require(path.join(cwd, m.url))
158
+ m.type = 'post'
159
+ m.url = `/${m.name}`
160
+ m.worker = false
161
+ })
162
+ }
163
+ console.log('Schema:', schema)
164
+
165
+
166
+ // Generate description block
167
+ if (description) {
168
+ const descriptionMd = fs.readFileSync(path.join(cwd, description), 'utf8')
169
+ descriptionHtml = converter.makeHtml(descriptionMd)
170
+
171
+ if (descriptionMd.includes('---')) {
172
+ descriptionTxt = descriptionMd
173
+ .split('---')[0]
174
+ .replace(/\n/g, ' ')
175
+ .replace(/\s+/g, ' ')
176
+ .replace(/#/g, '')
177
+ .replace(/\*/g, '')
178
+ .trim()
179
+ }
180
+ }
181
+
182
+ descriptionHtml += genHtmlFromSchema(schema)
183
+
184
+
185
+
186
+ // Generate jsee code
187
+ let jseeHtml = ''
188
+ let hiddenElementHtml = ''
189
+ if (argv.fetch) {
190
+ // Fetch jsee code from the CDN or local server
191
+ let jseeCode
192
+ if (argv.version === 'dev') {
193
+ jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.js'), 'utf8')
194
+ } else if (argv.version === 'latest') {
195
+ jseeCode = fs.readFileSync(path.join(__dirname, '..', 'dist', 'jsee.runtime.js'), 'utf8')
196
+ } else {
197
+ // Pre-fetch the jsee runtime from the CDN https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js
198
+ jseeCode = await fetch(`https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js`)
199
+ jseeCode = await jseeCode.text()
200
+ }
201
+ jseeHtml = `<script>${jseeCode}</script>`
202
+ // Fetch model files and store them in hidden elements
203
+ hiddenElementHtml += '<div id="hidden-storage" style="display: none;">'
204
+
205
+ for (let m of schema.model) {
206
+ if (m.type === 'get' || m.type === 'post') {
207
+ continue
208
+ }
209
+ if (m.url) {
210
+ const modelCode = fs.readFileSync(path.join(cwd, m.url), 'utf8')
211
+ hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${m.url}">${modelCode}</script>`
212
+ }
213
+ if (m.imports) {
214
+ for (let i of m.imports) {
215
+ const importUrl = i.includes('.js') ? i : `https://cdn.jsdelivr.net/npm/${i}`
216
+
217
+ // Create cache directory if it doesn't exist
218
+ const cacheDir = path.join(os.homedir(), '.cache', 'jsee')
219
+ fs.mkdirSync(cacheDir, { recursive: true })
220
+
221
+ // Create a hash of the importUrl
222
+ const hash = crypto.createHash('sha256').update(importUrl).digest('hex')
223
+ const cacheFilePath = path.join(cacheDir, `${hash}.js`)
224
+
225
+ let importCode
226
+ let useCache = false
227
+
228
+ // Check if cache file exists and is less than 1 day old
229
+ if (fs.existsSync(cacheFilePath)) {
230
+ const stats = fs.statSync(cacheFilePath)
231
+ const mtime = new Date(stats.mtime)
232
+ const now = new Date()
233
+ const ageInDays = (now - mtime) / (1000 * 60 * 60 * 24)
234
+
235
+ if (ageInDays < 1) {
236
+ console.log('Using cached import:', importUrl)
237
+ importCode = fs.readFileSync(cacheFilePath, 'utf8');
238
+ useCache = true;
239
+ }
240
+ }
241
+
242
+ if (!useCache) {
243
+ const response = await fetch(importUrl);
244
+ if (!response.ok) {
245
+ console.error(`Failed to fetch ${importUrl}: ${response.statusText}`);
246
+ process.exit(1);
247
+ }
248
+ importCode = await response.text()
249
+ fs.writeFileSync(cacheFilePath, importCode, 'utf8')
250
+ console.log('Fetched and stored to cache:', importUrl)
251
+ }
252
+ hiddenElementHtml += `<script type="text/plain" style="display: none;" data-src="${importUrl}">${importCode}</script>`
253
+ }
254
+ }
255
+ }
256
+ hiddenElementHtml += '</div>'
257
+ } else {
258
+ jseeHtml = outputs
259
+ ? `<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@${argv.version}/dist/jsee.runtime.js"></script>`
260
+ : `<script src="http://localhost:${argv.port}/dist/jsee.runtime.js"></script>`
261
+ }
262
+
263
+ let socialHtml = ''
264
+ let gaHtml = ''
265
+ let orgHtml = ''
266
+
267
+ if (schema.page) {
268
+ if (schema.page.title) {
269
+ title = schema.page.title
270
+ }
271
+ if (schema.page.ga) {
272
+ gaHtml = `
273
+ <script id="ga-src" async src="https://www.googletagmanager.com/gtag/js?id=${schema.page.ga}"></script>
274
+ <script id="ga-body">
275
+ window['ga-disable-${schema.page.ga}'] = window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1";
276
+ window.dataLayer = window.dataLayer || [];
277
+ function gtag(){dataLayer.push(arguments);}
278
+ gtag('js', new Date());
279
+ gtag('config', '${schema.page.ga}');
280
+ </script>
281
+ `
282
+ }
283
+
284
+ // Social media links
285
+ if (schema.page.social) {
286
+ // iterate over dict with k, v pairs
287
+ for (let [name, url] of Object.entries(schema.page.social)) {
288
+ switch (name) {
289
+ case 'twitter':
290
+ socialHtml += `<li><a rel="me" href="https://twitter.com/${url}">Twitter</a></li>`
291
+ break
292
+ case 'github':
293
+ socialHtml += `<li><a rel="me" href="https://github.com/${url}">GitHub</a></li>`
294
+ break
295
+ case 'facebook':
296
+ socialHtml += `<li><a rel="me" href="https://www.facebook.com/${url}">Facebook</a></li>`
297
+ break
298
+ case 'linkedin':
299
+ socialHtml += `<li><a rel="me" href="https://www.linkedin.com/company/${url}">LinkedIn</a></li>`
300
+ break
301
+ case 'instagram':
302
+ socialHtml += `<li><a rel="me" href="https://www.instagram.com/${url}">Instagram</a></li>`
303
+ break
304
+ case 'youtube':
305
+ socialHtml += `<li><a rel="me" href="https://www.youtube.com/${url}">YouTube</a></li>`
306
+ break
307
+ default:
308
+ socialHtml += `<li><a rel="me" href="${s.url}">${s.name}</a></li>`
309
+ }
310
+ }
311
+ }
312
+
313
+ if (schema.page.org) {
314
+ orgHtml = `<div class="footer-org"><h4 class="footer-heading"><a href="${schema.page.org.url}">${schema.page.org.name}</a></h4>`
315
+ if (schema.page.org.description) {
316
+ orgHtml += `<p>${schema.page.org.description}</p>`
317
+ }
318
+ orgHtml += '</div>'
319
+ }
320
+
321
+ }
322
+
323
+ const html = template(schema, {
324
+ descriptionHtml: pad(descriptionHtml, 8, 1),
325
+ descriptionTxt: descriptionTxt,
326
+ gaHtml: pad(gaHtml, 2, 1),
327
+ jseeHtml: jseeHtml,
328
+ hiddenElementHtml: hiddenElementHtml,
329
+ socialHtml: pad(socialHtml, 2, 1),
330
+ orgHtml: pad(orgHtml, 2, 1),
331
+ })
332
+
333
+ if (returnHtml) {
334
+ // Return the html as a string
335
+ return html
336
+ } else if (outputs) {
337
+ // Store the html in the output file
338
+ for (let o of outputs) {
339
+ if (o === 'stdout') {
340
+ console.log(html)
341
+ } else if (o.includes('.html')) {
342
+ fs.writeFileSync(path.join(cwd, o), html)
343
+ } else if (o.includes('.json')) {
344
+ fs.writeFileSync(path.join(cwd, o), JSON.stringify(schema, null, 2))
345
+ } else if (o.includes('.md')) {
346
+ fs.writeFileSync(path.join(cwd, o), genMarkdownFromSchema(schema))
347
+ } else {
348
+ console.error('Invalid output file:', o)
349
+ }
350
+ }
351
+ fs.writeFileSync(path.join(cwd, outputs[0]), html)
352
+ } else {
353
+ // Serve the html
354
+ const express = require('express')
355
+ const app = express()
356
+ app.use(express.json())
357
+ if (argv.execute) {
358
+ // Create post endpoint for executing the model
359
+ schema.model.forEach(m => {
360
+ app.post(m.url, (req, res) => {
361
+ console.log(`Executing model: ${m.name}`)
362
+ if (m.name in modelFuncs) {
363
+ const modelFunc = modelFuncs[m.name]
364
+ try {
365
+ const result = modelFunc(req.body)
366
+ res.json(result)
367
+ } catch (error) {
368
+ console.error('Error executing model:', error)
369
+ res.status(500).json({ error: error.message })
370
+ }
371
+ }
372
+ })
373
+ console.log('Model execution endpoints created:', m.url)
374
+ })
375
+ }
376
+ app.get('/', async (req, res) => {
377
+ console.log('Serving index.html')
378
+ res.send(await gen(process.argv, true))
379
+ })
380
+ app.get('/dist/jsee.runtime.js', (req, res) => {
381
+ const pathToJSEE = path.join(__dirname, '..', 'dist', 'jsee.runtime.js')
382
+ console.log(`Serving jsee.runtime.js from ${pathToJSEE}`)
383
+ res.sendFile(pathToJSEE)
384
+ })
385
+ app.use(express.static(cwd))
386
+ app.listen(argv.port, () => {
387
+ console.log(`JSEE app is running: http://localhost:${argv.port}`)
388
+ })
389
+ }
390
+ }
391
+
392
+ function genSchema (jsdocData) {
393
+ let schema = {
394
+ model: [],
395
+ inputs: [],
396
+ outputs: [],
397
+ }
398
+ for (let d of jsdocData) {
399
+ const model = {
400
+ name: d.name ? d.name : d.meta.filename.split('.')[0],
401
+ description: d.description ? d.description : '',
402
+ type: d.kind,
403
+ container: 'args',
404
+ url: path.relative(process.cwd(), path.join(d.meta.path, d.meta.filename)),
405
+ worker: false
406
+ }
407
+ if (d.requires) {
408
+ model.imports = d.requires.map(r => r.replace('module:', ''))
409
+ }
410
+ if (d.params) {
411
+ // Check if all params have the same name before '.'
412
+ const names = new Set(d.params.map(p => p.name.split('.')[0]))
413
+ if ((d.params.length > 1) && (names.size === 1)) {
414
+ // Object
415
+ model.container = 'object'
416
+ d.params.slice(1).forEach(p => {
417
+ const inp = {
418
+ name: p.name.split('.')[1],
419
+ type: p.type.names[0],
420
+ description: p.description,
421
+ }
422
+ if (p.defaultvalue) {
423
+ inp.default = p.defaultvalue
424
+ }
425
+ schema.inputs.push(inp)
426
+ })
427
+ } else {
428
+ // Array
429
+ model.container = 'args'
430
+ d.params.forEach(p => {
431
+ const inp = {
432
+ name: p.name,
433
+ type: p.type.names[0],
434
+ description: p.description,
435
+ }
436
+ if (p.defaultvalue) {
437
+ inp.default = p.defaultvalue
438
+ }
439
+ schema.inputs.push(inp)
440
+ })
441
+ }
442
+ }
443
+ if (d.returns) {
444
+ d.returns.forEach(r => {
445
+ r.name = r.name ? r.name : r.description.split('-')[0].trim()
446
+ r.description = r.description.split('-').slice(1).join('-').trim()
447
+ })
448
+ const names = new Set(d.returns.map(r => r.name.split('.')[0]))
449
+ if ((d.returns.length > 1) && (names.size === 1)) {
450
+ // Object
451
+ d.returns.slice(1).forEach(p => {
452
+ const out = {
453
+ name: p.name.split('.')[1],
454
+ type: p.type.names[0],
455
+ description: p.description,
456
+ }
457
+ schema.outputs.push(out)
458
+ })
459
+ } else {
460
+ // Array
461
+ d.returns.forEach(p => {
462
+ const out = {
463
+ name: p.name,
464
+ type: p.type.names[0],
465
+ description: p.description,
466
+ }
467
+ schema.outputs.push(out)
468
+ })
469
+ }
470
+ }
471
+ if (d.customTags) {
472
+ d.customTags.forEach(t => {
473
+ if (t.tag === 'worker') {
474
+ model.worker = true
475
+ }
476
+ })
477
+ }
478
+ schema.model.push(model)
479
+ }
480
+ return schema
481
+ }
482
+
483
+ function genHtmlFromSchema(schema) {
484
+ let htmlDescription = '<br><div class="schema-description">';
485
+
486
+ // Process the model section
487
+ if (schema.model && schema.model.length > 0) {
488
+ schema.model.forEach(model => {
489
+ htmlDescription += `<h3><strong>${model.name}</strong></h3>`
490
+ if (model.description) {
491
+ htmlDescription += `<p>${model.description}</p>`
492
+ }
493
+ })
494
+ }
495
+
496
+ // Process the inputs section
497
+ if (schema.inputs && schema.inputs.length > 0) {
498
+ htmlDescription += '<h4>Inputs</h4><ul>';
499
+ schema.inputs.forEach(input => {
500
+ htmlDescription += `<li><strong>${input.name}</strong> (${input.type})`
501
+ if (input.description) {
502
+ htmlDescription += ` - ${input.description}`
503
+ }
504
+ })
505
+ htmlDescription += '</ul>';
506
+ }
507
+
508
+ // Process the outputs section
509
+ if (schema.outputs && schema.outputs.length > 0) {
510
+ htmlDescription += '<h4>Outputs</h4><ul>';
511
+ schema.outputs.forEach(output => {
512
+ htmlDescription += `<li><strong>${output.name}</strong> (${output.type})`
513
+ if (output.description) {
514
+ htmlDescription += ` - ${output.description}`
515
+ }
516
+ })
517
+ htmlDescription += '</ul>';
518
+ }
519
+ htmlDescription += '</div>';
520
+ return htmlDescription;
521
+ }
522
+
523
+ function genMarkdownFromSchema(schema) {
524
+ let markdownDescription = '';
525
+
526
+ // Process the model section
527
+ if (schema.model && schema.model.length > 0) {
528
+ schema.model.forEach(model => {
529
+ markdownDescription += `### **${model.name}**\n`;
530
+ if (model.description) {
531
+ markdownDescription += `${model.description}\n\n`;
532
+ }
533
+ });
534
+ }
535
+
536
+ // Process the inputs section
537
+ if (schema.inputs && schema.inputs.length > 0) {
538
+ markdownDescription += '#### Inputs\n';
539
+ schema.inputs.forEach(input => {
540
+ markdownDescription += `- **${input.name}** (${input.type})`;
541
+ if (input.description) {
542
+ markdownDescription += ` - ${input.description}`;
543
+ }
544
+ markdownDescription += '\n';
545
+ });
546
+ }
547
+
548
+ // Process the outputs section
549
+ if (schema.outputs && schema.outputs.length > 0) {
550
+ markdownDescription += '#### Outputs\n';
551
+ schema.outputs.forEach(output => {
552
+ markdownDescription += `- **${output.name}** (${output.type})`;
553
+ if (output.description) {
554
+ markdownDescription += ` - ${output.description}`;
555
+ }
556
+ markdownDescription += '\n';
557
+ });
558
+ }
559
+
560
+ return markdownDescription;
561
+ }
562
+
563
+ function template(schema, blocks) {
564
+ let title = 'jsee'
565
+ // let url = schema.page && schema.page.url ? schema.page.url : ''
566
+ let url = ('page' in schema && 'url' in schema.page) ? schema.page.url : ''
567
+ if (schema.title) {
568
+ title = schema.title
569
+ } else if (schema.page && schema.page.title) {
570
+ title = schema.page.title
571
+ } else if (schema.model) {
572
+ if (Array.isArray(schema.model)) {
573
+ title = schema.model[0].name
574
+ } else {
575
+ title = schema.model.name
576
+ }
577
+ }
578
+
579
+ return `<!DOCTYPE html>
580
+
581
+ <!-- Generated by JSEE (https://jsee.org) -->
582
+ <!-- Do not edit this file directly. Edit the source files and run jsee to generate this file. -->
583
+ <!-- License: MIT (https://opensource.org/licenses/MIT) -->
584
+
585
+ <html lang="en">
586
+ <head>
587
+ <meta charset="utf-8">
588
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
589
+ <meta name="viewport" content="width=device-width, initial-scale=1">
590
+ <title>${title}</title>
591
+ <meta name="description" content="${blocks.descriptionTxt}">
592
+
593
+ <!-- Open Graph -->
594
+ <meta property="og:title" content="${title}" />
595
+ <meta property="og:description" content="${blocks.descriptionTxt}" />
596
+ <meta property="og:locale" content="en_US" />
597
+ <meta property="og:url" content="${url}" />
598
+ <meta property="og:site_name" content="${title}" />
599
+ <meta property="og:type" content="website" />
600
+
601
+ <!-- Twitter Card -->
602
+ <meta name="twitter:card" content="summary" />
603
+ <meta name="twitter:title" content="${title}" />
604
+ <meta name="twitter:description" content="${blocks.descriptionTxt}" />
605
+
606
+ <!-- Structured Data -->
607
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","headline":"${title}","name":"${title}","url":"${url}", "description":"${blocks.descriptionTxt}"}</script>
608
+
609
+ <!-- Canonical Link -->
610
+ <link rel="canonical" href="${url}" />
611
+
612
+ <!-- Favicon -->
613
+ <link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAPj4+AAMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAABERAAAAAAAAEREAAAAAAAAREQABERESABERAAETMzAAEREAARAAAAAREQABEAAAABERAAEQARAAEREAARABEAAREQABEAAAABERAAEQAAAAEREAAREREAAREQABEREQABERAAAAAAAAEREAAAAAAAAREQAAAAAAABHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" />
614
+
615
+ <!-- Styles -->
616
+ <style>
617
+ /** Main */
618
+ html { font-size: 16px; }
619
+ body, h1, h2, h3, h4, h5, h6, p, blockquote, pre, hr, dl, dd, ol, ul, figure { margin: 0; padding: 0; }
620
+ body { font: 400 16px/1.5 -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Segoe UI Symbol", "Segoe UI Emoji", "Apple Color Emoji", Roboto, Helvetica, Arial, sans-serif; color: #111111; background-color: #fdfdfd; -webkit-text-size-adjust: 100%; -webkit-font-feature-settings: "kern" 1; -moz-font-feature-settings: "kern" 1; -o-font-feature-settings: "kern" 1; font-feature-settings: "kern" 1; font-kerning: normal; display: flex; min-height: 100vh; flex-direction: column; overflow-wrap: break-word; }
621
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre, ul, ol, dl, figure, .highlight { margin-bottom: 15px; }
622
+ hr { margin-top: 30px; margin-bottom: 30px; border: 0; border-top: 1px solid #ececec; }
623
+ main { display: block; /* Default value of display of main element is 'inline' in IE 11. */ }
624
+ img { max-width: 100%; vertical-align: middle; }
625
+ figure > img { display: block; }
626
+ figcaption { font-size: 14px; }
627
+ ul, ol { margin-left: 30px; }
628
+ li > ul, li > ol { margin-bottom: 0; }
629
+ h1, h2, h3, h4, h5, h6 { font-weight: 400; }
630
+ a { color: #2a7ae2; text-decoration: none; }
631
+ a:visited { color: #1756a9; }
632
+ a:hover { color: #111111; text-decoration: underline; }
633
+ .social-media-list a:hover, .pagination a:hover { text-decoration: none; }
634
+ .social-media-list a:hover .username, .pagination a:hover .username { text-decoration: underline; }
635
+ blockquote { color: #828282; border-left: 4px solid #e8e8e8; padding-left: 15px; font-size: 1.125rem; font-style: italic; }
636
+ blockquote > :last-child { margin-bottom: 0; }
637
+ blockquote i, blockquote em { font-style: normal; }
638
+ pre, code { font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace; font-size: 0.9375em; border: 1px solid #e8e8e8; border-radius: 3px; background-color: #eeeeff; }
639
+ code { padding: 1px 5px; }
640
+ pre { padding: 8px 12px; overflow-x: auto; }
641
+ pre > code { border: 0; padding-right: 0; padding-left: 0; }
642
+ .wrapper { max-width: calc(800px - (30px)); margin-right: auto; margin-left: auto; padding-right: 15px; padding-left: 15px; }
643
+ @media screen and (min-width: 800px) { .wrapper { max-width: calc(800px - (30px * 2)); padding-right: 30px; padding-left: 30px; } }
644
+ .wrapper:after { content: ""; display: table; clear: both; }
645
+ .orange { color: #f66a0a; }
646
+ .grey { color: #828282; }
647
+ .svg-icon { width: 16px; height: 16px; display: inline-block; fill: currentColor; padding: 5px 3px 2px 5px; vertical-align: text-bottom; }
648
+ table { margin-bottom: 30px; width: 100%; text-align: left; color: #3f3f3f; border-collapse: collapse; border: 1px solid #e8e8e8; }
649
+ table tr:nth-child(even) { background-color: #f7f7f7; }
650
+ table th, table td { padding: 10px 15px; }
651
+ table th { background-color: #f0f0f0; border: 1px solid #e0e0e0; }
652
+ table td { border: 1px solid #e8e8e8; }
653
+ @media screen and (max-width: 800px) { table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } }
654
+ .site-header { border-bottom: 1px solid #e8e8e8; min-height: 55.95px; line-height: 54px; position: relative; }
655
+ .site-title { font-size: 1.625rem; font-weight: 800; letter-spacing: -1px; margin-bottom: 0; float: left; }
656
+ @media screen and (max-width: 600px) { .site-title { padding-right: 45px; } }
657
+ .site-title, .site-title:visited { color: #424242; }
658
+ .site-nav { position: absolute; top: 9px; right: 15px; background-color: #fdfdfd; border: 1px solid #e8e8e8; border-radius: 5px; text-align: right; }
659
+ .site-nav .nav-trigger { display: none; }
660
+ .site-nav .menu-icon { float: right; width: 36px; height: 26px; line-height: 0; padding-top: 10px; text-align: center; }
661
+ .site-nav .menu-icon > svg path { fill: #424242; }
662
+ .site-nav label[for="nav-trigger"] { display: block; float: right; width: 36px; height: 36px; z-index: 2; cursor: pointer; }
663
+ .site-nav input ~ .trigger { clear: both; display: none; }
664
+ .site-nav input:checked ~ .trigger { display: block; padding-bottom: 5px; }
665
+ .site-nav .page-link { color: #111111; line-height: 1.5; display: block; padding: 5px 10px; margin-left: 20px; }
666
+ .site-nav .page-link:not(:last-child) { margin-right: 0; }
667
+ @media screen and (min-width: 600px) { .site-nav { position: static; float: right; border: none; background-color: inherit; } .site-nav label[for="nav-trigger"] { display: none; } .site-nav .menu-icon { display: none; } .site-nav input ~ .trigger { display: block; } .site-nav .page-link { display: inline; padding: 0; margin-left: auto; } .site-nav .page-link:not(:last-child) { margin-right: 20px; } }
668
+ .site-footer { border-top: 1px solid #e8e8e8; padding: 30px 0; }
669
+ .footer-heading { font-size: 1.7rem; line-height: 1.7rem; font-weight: 200; margin-bottom: 5px;}
670
+ .footer-heading a { color: #a2a2a2; text-decoration: none; }
671
+ .footer-heading a:hover { color: #828282; }
672
+ .footer-org p { font-size: 0.65rem; color: #828282; }
673
+ .feed-subscribe .svg-icon { padding: 5px 5px 2px 0; }
674
+ .contact-list, .social-media-list, .pagination { list-style: none; margin-left: 0; }
675
+ .footer-col-wrapper, .social-links { font-size: 0.9375rem; color: #828282; }
676
+ .footer-col { margin-bottom: 15px; }
677
+ .footer-col-1, .footer-col-2 { width: calc(50% - (30px / 2)); }
678
+ .footer-col-3 { width: calc(100% - (30px / 2)); }
679
+ @media screen and (min-width: 800px) { .footer-col-1 { width: calc(35% - (30px / 2)); } .footer-col-2 { width: calc(20% - (30px / 2)); } .footer-col-3 { width: calc(45% - (30px / 2)); } }
680
+ @media screen and (min-width: 600px) { .footer-col-wrapper { display: flex; } .footer-col { width: calc(100% - (30px / 2)); padding: 0 15px; } .footer-col:first-child { padding-right: 15px; padding-left: 0; } .footer-col:last-child { padding-right: 0; padding-left: 15px; } }
681
+ /** Page content */
682
+ .page-content { padding: 30px 0; flex: 1 0 auto; }
683
+ .page-heading { font-size: 2rem; }
684
+ .post-list-heading { font-size: 1.75rem; }
685
+ .post-list { margin-left: 0; list-style: none; }
686
+ .post-list > li { margin-bottom: 30px; }
687
+ .post-meta { font-size: 14px; color: #828282; }
688
+ .post-link { display: block; font-size: 1.5rem; }
689
+ /** Posts */
690
+ .post-header { margin-bottom: 30px; }
691
+ .post-title, .post-content h1 { font-size: 2.625rem; letter-spacing: -1px; line-height: 1.15; }
692
+ @media screen and (min-width: 800px) { .post-title, .post-content h1 { font-size: 2.625rem; } }
693
+ .post-content { margin-bottom: 30px; }
694
+ .post-content h1, .post-content h2, .post-content h3 { margin-top: 60px; }
695
+ .post-content h4, .post-content h5, .post-content h6 { margin-top: 30px; }
696
+ .post-content h2 { font-size: 1.75rem; }
697
+ @media screen and (min-width: 800px) { .post-content h2 { font-size: 2rem; } }
698
+ .post-content h3 { font-size: 1.375rem; }
699
+ @media screen and (min-width: 800px) { .post-content h3 { font-size: 1.625rem; } }
700
+ .post-content h4 { font-size: 1.25rem; }
701
+ .post-content h5 { font-size: 1.125rem; }
702
+ .post-content h6 { font-size: 1.0625rem; }
703
+ .social-media-list, .pagination { display: table; margin: 0 auto; }
704
+ .social-media-list li, .pagination li { float: left; margin: 5px 10px 5px 0; }
705
+ .social-media-list li:last-of-type, .pagination li:last-of-type { margin-right: 0; }
706
+ .social-media-list li a, .pagination li a { display: block; padding: 7.5px; border: 1px solid #e8e8e8; }
707
+ .social-media-list li a:hover, .pagination li a:hover { border-color: #dbdbdb; }
708
+ /** Pagination navbar */
709
+ .pagination { margin-bottom: 30px; }
710
+ .pagination li a, .pagination li div { min-width: 41px; text-align: center; box-sizing: border-box; }
711
+ .pagination li div { display: block; padding: 7.5px; border: 1px solid transparent; }
712
+ .pagination li div.pager-edge { color: #e8e8e8; border: 1px dashed; }
713
+ /** Grid helpers */
714
+ @media screen and (min-width: 800px) { .one-half { width: calc(50% - (30px / 2)); } }
715
+ /** Jsee elements */
716
+ .app-container { background-color: #F0F1F4; border-bottom: 1px solid #e8e8e8; padding-bottom: 55px }
717
+ #download-btn { float: right; margin-top: 10px; padding: 10px; background-color: white; border: none; cursor: pointer; }
718
+ #download-btn:hover { background-color: #f0f0f0; }
719
+ .schema-description { background-color: #f8f8fa; padding: 20px; margin-top: 20px; border-radius: 10px; border: 1px solid #e8e8e8; }
720
+ .schema-description h2, .schema-description h3, .schema-description h4 { margin-top: 10px; }
721
+ /** Logos */
722
+ .logo_footer { margin-left: -3px; }
723
+ .logo_footer svg { opacity: 0.35; }
724
+ .logo_footer:hover svg { opacity: 1; }
725
+ .social-links { display: flex; justify-content: right; }
726
+ .social-links .social-media-list, .social-links .pagination { margin: 0; }
727
+ </style>
728
+ <link type="application/atom+xml" rel="alternate" href="/feed.xml" title="hashr" />
729
+ ${blocks.gaHtml}
730
+ </head>
731
+ <body>
732
+ ${blocks.hiddenElementHtml}
733
+ <header class="site-header">
734
+ <div class="wrapper">
735
+ <span class="site-title">${title}</span>
736
+ <button id="download-btn" title="Download bundled HTML file without external dependencies to use offline">Download bundle (html)</button>
737
+ </div>
738
+ </header>
739
+ <div class="page-content app-container">
740
+ <div class="wrapper">
741
+ <div id="jsee-container"></div>
742
+ </div>
743
+ </div>
744
+ <main class="page-content" aria-label="Content">
745
+ <div class="wrapper">
746
+ <article class="post">
747
+ <div class="post-content">
748
+ ${blocks.descriptionHtml}
749
+ </div>
750
+ </article>
751
+ </div>
752
+ </main>
753
+ <footer class="site-footer h-card">
754
+ <data class="u-url" href="/"></data>
755
+ <div class="wrapper">
756
+ <div class="footer-col-wrapper">
757
+ <div class="footer-col">
758
+ ${blocks.orgHtml}
759
+ </div>
760
+ <div class="footer-col">
761
+ <div class="social-links">
762
+ <ul class="social-media-list">
763
+ ${blocks.socialHtml}
764
+ </ul>
765
+ </div>
766
+ </div>
767
+ </div>
768
+ </div>
769
+ </footer>
770
+ ${blocks.jseeHtml}
771
+ <script>
772
+ const schema = ${JSON.stringify(schema, null, 2)}
773
+ const title = "${title}"
774
+ var env = new JSEE({
775
+ container: document.getElementById('jsee-container'),
776
+ schema
777
+ })
778
+ document.getElementById('download-btn').addEventListener('click', async () => {
779
+ env.download(title)
780
+ })
781
+ </script>
782
+ </body>
783
+ </html>`
784
+ }
785
+
786
+ module.exports = gen