boxwood 2.6.0 → 2.8.2
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/index.d.ts +569 -420
- package/index.js +179 -59
- package/package.json +5 -5
- package/ui/markdown/index.js +174 -35
package/index.js
CHANGED
|
@@ -3,6 +3,70 @@ const { readFileSync, realpathSync, lstatSync } = require("fs")
|
|
|
3
3
|
const csstree = require("css-tree")
|
|
4
4
|
const { createHash } = require("./utilities/hash")
|
|
5
5
|
|
|
6
|
+
class TranslationError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = "TranslationError"
|
|
10
|
+
Error.captureStackTrace(this, this.constructor)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class FileError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message)
|
|
17
|
+
this.name = "FileError"
|
|
18
|
+
Error.captureStackTrace(this, this.constructor)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class RawError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message)
|
|
25
|
+
this.name = "RawError"
|
|
26
|
+
Error.captureStackTrace(this, this.constructor)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class CSSError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message)
|
|
33
|
+
this.name = "CSSError"
|
|
34
|
+
Error.captureStackTrace(this, this.constructor)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ImageError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message)
|
|
41
|
+
this.name = "ImageError"
|
|
42
|
+
Error.captureStackTrace(this, this.constructor)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class SVGError extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message)
|
|
49
|
+
this.name = "SVGError"
|
|
50
|
+
Error.captureStackTrace(this, this.constructor)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class JSONError extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message)
|
|
57
|
+
this.name = "JSONError"
|
|
58
|
+
Error.captureStackTrace(this, this.constructor)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class ComponentError extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message)
|
|
65
|
+
this.name = "ComponentError"
|
|
66
|
+
Error.captureStackTrace(this, this.constructor)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
6
70
|
function compile(path) {
|
|
7
71
|
const fn = require(path)
|
|
8
72
|
return {
|
|
@@ -37,7 +101,7 @@ function compile(path) {
|
|
|
37
101
|
if (
|
|
38
102
|
attributes.src ||
|
|
39
103
|
["application/json", "application/ld+json"].includes(
|
|
40
|
-
attributes.type
|
|
104
|
+
attributes.type,
|
|
41
105
|
)
|
|
42
106
|
) {
|
|
43
107
|
node.ignore = false
|
|
@@ -179,7 +243,7 @@ function validateSymlinks(path, base) {
|
|
|
179
243
|
if (!part) continue
|
|
180
244
|
current = resolve(current, part)
|
|
181
245
|
if (lstatSync(current).isSymbolicLink()) {
|
|
182
|
-
throw new
|
|
246
|
+
throw new FileError(`symlinks are not allowed ("${current}")`)
|
|
183
247
|
}
|
|
184
248
|
}
|
|
185
249
|
}
|
|
@@ -191,33 +255,31 @@ function validateFile(path, base) {
|
|
|
191
255
|
const type = extension(normalizedPath)
|
|
192
256
|
|
|
193
257
|
if (!type) {
|
|
194
|
-
throw new
|
|
258
|
+
throw new FileError(`path "${path}" has no extension`)
|
|
195
259
|
}
|
|
196
260
|
|
|
197
261
|
if (!ALLOWED_READ_EXTENSIONS.includes(type)) {
|
|
198
|
-
throw new
|
|
199
|
-
`FileError: unsupported file type "${type}" for path "${path}"`
|
|
200
|
-
)
|
|
262
|
+
throw new FileError(`unsupported file type "${type}" for path "${path}"`)
|
|
201
263
|
}
|
|
202
264
|
|
|
203
265
|
const stats = lstatSync(normalizedPath)
|
|
204
266
|
if (!stats.isFile()) {
|
|
205
|
-
throw new
|
|
267
|
+
throw new FileError(`path "${path}" is not a file`)
|
|
206
268
|
}
|
|
207
269
|
|
|
208
270
|
if (stats.isSymbolicLink()) {
|
|
209
|
-
throw new
|
|
271
|
+
throw new FileError(`path "${path}" is a symbolic link`)
|
|
210
272
|
}
|
|
211
273
|
|
|
212
274
|
if (normalizedPath === normalizedBase) {
|
|
213
|
-
throw new
|
|
214
|
-
`
|
|
275
|
+
throw new FileError(
|
|
276
|
+
`path "${path}" is the same as the current working directory "${base}"`,
|
|
215
277
|
)
|
|
216
278
|
}
|
|
217
279
|
|
|
218
280
|
if (!normalizedPath.startsWith(normalizedBase + "/")) {
|
|
219
|
-
throw new
|
|
220
|
-
`
|
|
281
|
+
throw new FileError(
|
|
282
|
+
`real path "${normalizedPath}" is not within the current working directory "${normalizedBase}"`,
|
|
221
283
|
)
|
|
222
284
|
}
|
|
223
285
|
}
|
|
@@ -235,9 +297,7 @@ function readFile(path, encoding) {
|
|
|
235
297
|
|
|
236
298
|
return readFileSync(path, encoding)
|
|
237
299
|
} catch (exception) {
|
|
238
|
-
throw new
|
|
239
|
-
`FileError: cannot read file "${path}": ${exception.message}`
|
|
240
|
-
)
|
|
300
|
+
throw new FileError(`cannot read file "${path}": ${exception.message}`)
|
|
241
301
|
}
|
|
242
302
|
}
|
|
243
303
|
|
|
@@ -286,7 +346,7 @@ const ALIASES = {
|
|
|
286
346
|
}
|
|
287
347
|
|
|
288
348
|
// Pre-compiled regex for better performance
|
|
289
|
-
const KEY_VALIDATION_REGEX = /^[a-zA-Z0-9\-_]+$/
|
|
349
|
+
const KEY_VALIDATION_REGEX = /^[a-zA-Z0-9\-_:]+$/
|
|
290
350
|
const isKeyValid = (key) => KEY_VALIDATION_REGEX.test(key)
|
|
291
351
|
|
|
292
352
|
const attributes = (options) => {
|
|
@@ -309,7 +369,6 @@ const attributes = (options) => {
|
|
|
309
369
|
result.push(key)
|
|
310
370
|
} else {
|
|
311
371
|
const name = ALIASES[key] || key
|
|
312
|
-
const value = options[key]
|
|
313
372
|
const content = Array.isArray(value) ? classes(...value) : value
|
|
314
373
|
result.push(`${name}="${escapeHTML(content)}"`)
|
|
315
374
|
}
|
|
@@ -330,8 +389,8 @@ const attributes = (options) => {
|
|
|
330
389
|
const left = result.left || "0"
|
|
331
390
|
styles.push(
|
|
332
391
|
`${decamelize(param)}:${escapeHTML(
|
|
333
|
-
`${top} ${right} ${bottom} ${left}
|
|
334
|
-
)}
|
|
392
|
+
`${top} ${right} ${bottom} ${left}`,
|
|
393
|
+
)}`,
|
|
335
394
|
)
|
|
336
395
|
} else if (typeof result === "string" || typeof result === "number") {
|
|
337
396
|
styles.push(`${decamelize(param)}:${escapeHTML(result)}`)
|
|
@@ -378,7 +437,7 @@ const render = (input, escape = true) => {
|
|
|
378
437
|
if (Array.isArray(input)) {
|
|
379
438
|
let result = ""
|
|
380
439
|
for (let i = 0, ilen = input.length; i < ilen; i++) {
|
|
381
|
-
result += render(input[i])
|
|
440
|
+
result += render(input[i], escape)
|
|
382
441
|
}
|
|
383
442
|
return result
|
|
384
443
|
}
|
|
@@ -480,9 +539,7 @@ const sanitizeHTML = (content) => {
|
|
|
480
539
|
raw.load = function (path, options = {}) {
|
|
481
540
|
const type = extension(path)
|
|
482
541
|
if (!ALLOWED_RAW_EXTENSIONS.includes(type)) {
|
|
483
|
-
throw new
|
|
484
|
-
`RawError: unsupported raw type "${type}" for path "${path}"`
|
|
485
|
-
)
|
|
542
|
+
throw new RawError(`unsupported raw type "${type}" for path "${path}"`)
|
|
486
543
|
}
|
|
487
544
|
|
|
488
545
|
let content = readFile(path, "utf8")
|
|
@@ -495,22 +552,48 @@ raw.load = function (path, options = {}) {
|
|
|
495
552
|
return raw(content)
|
|
496
553
|
}
|
|
497
554
|
|
|
498
|
-
const tag = (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
555
|
+
const tag = (tagName, attrsOrChildren, ...restChildren) => {
|
|
556
|
+
// Check if second argument is children (not attributes)
|
|
557
|
+
const isChildrenNotAttributes =
|
|
558
|
+
typeof attrsOrChildren === "string" ||
|
|
559
|
+
typeof attrsOrChildren === "number" ||
|
|
560
|
+
Array.isArray(attrsOrChildren) ||
|
|
561
|
+
(attrsOrChildren &&
|
|
562
|
+
typeof attrsOrChildren === "object" &&
|
|
563
|
+
"name" in attrsOrChildren &&
|
|
564
|
+
"children" in attrsOrChildren)
|
|
565
|
+
|
|
566
|
+
// If we have rest arguments, they must be additional children
|
|
567
|
+
if (restChildren.length > 0) {
|
|
568
|
+
if (isChildrenNotAttributes) {
|
|
569
|
+
// tagName is name, attrsOrChildren is first child, restChildren are more children
|
|
570
|
+
return {
|
|
571
|
+
name: tagName,
|
|
572
|
+
children: [attrsOrChildren, ...restChildren],
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
// tagName is name, attrsOrChildren is attributes, restChildren are children
|
|
576
|
+
return {
|
|
577
|
+
name: tagName,
|
|
578
|
+
attributes: attrsOrChildren,
|
|
579
|
+
children: restChildren,
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Original two-argument logic
|
|
585
|
+
if (isChildrenNotAttributes) {
|
|
502
586
|
return {
|
|
503
|
-
name,
|
|
504
|
-
children:
|
|
587
|
+
name: tagName,
|
|
588
|
+
children: attrsOrChildren,
|
|
505
589
|
}
|
|
506
590
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const children = typeof c === "number" ? c : c || []
|
|
591
|
+
|
|
592
|
+
// attrsOrChildren is attributes, no children provided
|
|
510
593
|
return {
|
|
511
|
-
name,
|
|
512
|
-
children,
|
|
513
|
-
attributes,
|
|
594
|
+
name: tagName,
|
|
595
|
+
children: [],
|
|
596
|
+
attributes: attrsOrChildren,
|
|
514
597
|
}
|
|
515
598
|
}
|
|
516
599
|
|
|
@@ -585,7 +668,7 @@ function css(inputs) {
|
|
|
585
668
|
}
|
|
586
669
|
}
|
|
587
670
|
|
|
588
|
-
function
|
|
671
|
+
function occurrences(input, string) {
|
|
589
672
|
if (string.length <= 0) {
|
|
590
673
|
return input.length + 1
|
|
591
674
|
}
|
|
@@ -606,8 +689,8 @@ function occurences(input, string) {
|
|
|
606
689
|
}
|
|
607
690
|
|
|
608
691
|
const validateCSS = (content, character1, character2) => {
|
|
609
|
-
const count1 =
|
|
610
|
-
const count2 =
|
|
692
|
+
const count1 = occurrences(content, character1)
|
|
693
|
+
const count2 = occurrences(content, character2)
|
|
611
694
|
if (count1 !== count2) {
|
|
612
695
|
return {
|
|
613
696
|
valid: false,
|
|
@@ -639,7 +722,7 @@ css.load = function (path) {
|
|
|
639
722
|
const content = readFile(file, "utf8")
|
|
640
723
|
const { valid, message } = isCSSValid(content)
|
|
641
724
|
if (!valid) {
|
|
642
|
-
throw new
|
|
725
|
+
throw new CSSError(`invalid CSS for path "${file}": ${message}`)
|
|
643
726
|
}
|
|
644
727
|
return css`
|
|
645
728
|
${content}
|
|
@@ -696,7 +779,10 @@ js.load = function (path, options = {}) {
|
|
|
696
779
|
return { js: tag("script", attributes, content) }
|
|
697
780
|
}
|
|
698
781
|
|
|
699
|
-
const node =
|
|
782
|
+
const node =
|
|
783
|
+
(name) =>
|
|
784
|
+
(options, ...children) =>
|
|
785
|
+
tag(name, options, ...children)
|
|
700
786
|
const Doctype = node("!DOCTYPE html")
|
|
701
787
|
|
|
702
788
|
const nodes = [
|
|
@@ -871,9 +957,7 @@ function base64({ content, path }) {
|
|
|
871
957
|
nodes.Img.load = function (path) {
|
|
872
958
|
const type = extension(path)
|
|
873
959
|
if (!ALLOWED_IMAGE_EXTENSIONS.includes(type)) {
|
|
874
|
-
throw new
|
|
875
|
-
`ImageError: unsupported image type "${type}" for path "${path}"`
|
|
876
|
-
)
|
|
960
|
+
throw new ImageError(`unsupported image type "${type}" for path "${path}"`)
|
|
877
961
|
}
|
|
878
962
|
const content = readFile(path, "base64")
|
|
879
963
|
return (options) => {
|
|
@@ -926,9 +1010,7 @@ const sanitizeSVG = (content) => {
|
|
|
926
1010
|
nodes.Svg.load = function (path, options = {}) {
|
|
927
1011
|
const type = extension(path)
|
|
928
1012
|
if (type !== "svg") {
|
|
929
|
-
throw new
|
|
930
|
-
`SVGError: unsupported SVG type "${type}" for path "${path}"`
|
|
931
|
-
)
|
|
1013
|
+
throw new SVGError(`unsupported SVG type "${type}" for path "${path}"`)
|
|
932
1014
|
}
|
|
933
1015
|
let content = readFile(path, "utf8")
|
|
934
1016
|
if (options.sanitize !== false) {
|
|
@@ -968,15 +1050,29 @@ const json = {
|
|
|
968
1050
|
try {
|
|
969
1051
|
return JSON.parse(content)
|
|
970
1052
|
} catch (exception) {
|
|
971
|
-
throw new
|
|
972
|
-
`JSONError: cannot parse file "${file}": ${exception.message}`
|
|
973
|
-
)
|
|
1053
|
+
throw new JSONError(`cannot parse file "${file}": ${exception.message}`)
|
|
974
1054
|
}
|
|
975
1055
|
},
|
|
976
1056
|
}
|
|
977
1057
|
|
|
978
1058
|
function i18n(translations) {
|
|
979
1059
|
return function translate(language, key) {
|
|
1060
|
+
if (key === undefined) {
|
|
1061
|
+
throw new TranslationError("key is undefined")
|
|
1062
|
+
}
|
|
1063
|
+
if (language === undefined) {
|
|
1064
|
+
throw new TranslationError("language is undefined")
|
|
1065
|
+
}
|
|
1066
|
+
if (translations[key] === undefined) {
|
|
1067
|
+
throw new TranslationError(
|
|
1068
|
+
`translation [${key}][${language}] is undefined`,
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
if (translations[key][language] === undefined) {
|
|
1072
|
+
throw new TranslationError(
|
|
1073
|
+
`translation [${key}][${language}] is undefined`,
|
|
1074
|
+
)
|
|
1075
|
+
}
|
|
980
1076
|
return translations[key][language]
|
|
981
1077
|
}
|
|
982
1078
|
}
|
|
@@ -995,14 +1091,14 @@ i18n.load = function (path, options = {}) {
|
|
|
995
1091
|
|
|
996
1092
|
return function translate(language, key) {
|
|
997
1093
|
if (!language) {
|
|
998
|
-
throw new
|
|
1094
|
+
throw new TranslationError(`language is undefined`)
|
|
999
1095
|
}
|
|
1000
1096
|
if (!key) {
|
|
1001
|
-
throw new
|
|
1097
|
+
throw new TranslationError(`key is undefined`)
|
|
1002
1098
|
}
|
|
1003
1099
|
if (!data[key] || !data[key][language]) {
|
|
1004
|
-
throw new
|
|
1005
|
-
`
|
|
1100
|
+
throw new TranslationError(
|
|
1101
|
+
`translation [${key}][${language}] is undefined`,
|
|
1006
1102
|
)
|
|
1007
1103
|
}
|
|
1008
1104
|
return data[key][language]
|
|
@@ -1016,20 +1112,20 @@ function component(fn, { styles, i18n, scripts } = {}) {
|
|
|
1016
1112
|
}
|
|
1017
1113
|
if (i18n) {
|
|
1018
1114
|
if (!a || !a.language) {
|
|
1019
|
-
throw new
|
|
1020
|
-
`
|
|
1115
|
+
throw new TranslationError(
|
|
1116
|
+
`language is undefined for component:\n${fn.toString()}`,
|
|
1021
1117
|
)
|
|
1022
1118
|
}
|
|
1023
1119
|
const { language } = a
|
|
1024
1120
|
function translate(key) {
|
|
1025
1121
|
if (!key) {
|
|
1026
|
-
throw new
|
|
1027
|
-
`
|
|
1122
|
+
throw new TranslationError(
|
|
1123
|
+
`key is undefined for component:\n${fn.toString()}`,
|
|
1028
1124
|
)
|
|
1029
1125
|
}
|
|
1030
1126
|
if (!i18n[key] || !i18n[key][language]) {
|
|
1031
|
-
throw new
|
|
1032
|
-
`
|
|
1127
|
+
throw new TranslationError(
|
|
1128
|
+
`translation [${key}][${language}] is undefined for component:\n${fn.toString()}`,
|
|
1033
1129
|
)
|
|
1034
1130
|
}
|
|
1035
1131
|
const translation = i18n[key][language]
|
|
@@ -1045,11 +1141,27 @@ function component(fn, { styles, i18n, scripts } = {}) {
|
|
|
1045
1141
|
|
|
1046
1142
|
if (styles) {
|
|
1047
1143
|
const data = Array.isArray(styles) ? styles : [styles]
|
|
1144
|
+
data.forEach((style, index) => {
|
|
1145
|
+
if (!style || typeof style !== "object" || !style.css) {
|
|
1146
|
+
throw new ComponentError(
|
|
1147
|
+
`Invalid style object at index ${index}: missing .css property. ` +
|
|
1148
|
+
`Styles must be created using the css\`...\` template tag or css.load() function.`,
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
})
|
|
1048
1152
|
nodes = nodes.concat(data.map((style) => style.css))
|
|
1049
1153
|
}
|
|
1050
1154
|
|
|
1051
1155
|
if (scripts) {
|
|
1052
1156
|
const data = Array.isArray(scripts) ? scripts : [scripts]
|
|
1157
|
+
data.forEach((script, index) => {
|
|
1158
|
+
if (!script || typeof script !== "object" || !script.js) {
|
|
1159
|
+
throw new ComponentError(
|
|
1160
|
+
`Invalid script object at index ${index}: missing .js property. ` +
|
|
1161
|
+
`Scripts must be created using the js\`...\` template tag or js.load() function.`,
|
|
1162
|
+
)
|
|
1163
|
+
}
|
|
1164
|
+
})
|
|
1053
1165
|
nodes = nodes.concat(data.map((script) => script.js))
|
|
1054
1166
|
}
|
|
1055
1167
|
|
|
@@ -1069,5 +1181,13 @@ module.exports = {
|
|
|
1069
1181
|
json,
|
|
1070
1182
|
tag,
|
|
1071
1183
|
i18n,
|
|
1184
|
+
TranslationError,
|
|
1185
|
+
ComponentError,
|
|
1186
|
+
FileError,
|
|
1187
|
+
RawError,
|
|
1188
|
+
CSSError,
|
|
1189
|
+
ImageError,
|
|
1190
|
+
SVGError,
|
|
1191
|
+
JSONError,
|
|
1072
1192
|
...nodes,
|
|
1073
1193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boxwood",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "Compile HTML templates into JS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"homepage": "https://github.com/buxlabs/boxwood#readme",
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"benchmark": "2.1.4",
|
|
50
|
-
"c8": "^
|
|
50
|
+
"c8": "^11.0.0",
|
|
51
51
|
"express": "^5.2.1",
|
|
52
52
|
"handlebars": "^4.7.8",
|
|
53
|
-
"jsdom": "^
|
|
53
|
+
"jsdom": "^28.1.0",
|
|
54
54
|
"mustache": "^4.2.0",
|
|
55
|
-
"underscore": "^1.13.
|
|
55
|
+
"underscore": "^1.13.8"
|
|
56
56
|
},
|
|
57
57
|
"standard": {
|
|
58
58
|
"ignore": [
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
]
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"css-tree": "^3.1
|
|
65
|
+
"css-tree": "^3.2.1"
|
|
66
66
|
},
|
|
67
67
|
"prettier": {
|
|
68
68
|
"semi": false
|
package/ui/markdown/index.js
CHANGED
|
@@ -1,42 +1,181 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
component,
|
|
3
|
+
H1,
|
|
4
|
+
H2,
|
|
5
|
+
H3,
|
|
6
|
+
H4,
|
|
7
|
+
H5,
|
|
8
|
+
H6,
|
|
9
|
+
P,
|
|
10
|
+
Blockquote,
|
|
11
|
+
Ul,
|
|
12
|
+
Ol,
|
|
13
|
+
Li,
|
|
14
|
+
Strong,
|
|
15
|
+
Em,
|
|
16
|
+
Code,
|
|
17
|
+
} = require("../..")
|
|
18
|
+
|
|
19
|
+
const ORDERED_LIST_REGEXP = /^\d+\.\s/
|
|
20
|
+
const UNORDERED_MARKERS = ["- ", "— ", "– ", "• "]
|
|
21
|
+
const HEADINGS = [
|
|
22
|
+
{ prefix: "###### ", type: "h6" },
|
|
23
|
+
{ prefix: "##### ", type: "h5" },
|
|
24
|
+
{ prefix: "#### ", type: "h4" },
|
|
25
|
+
{ prefix: "### ", type: "h3" },
|
|
26
|
+
{ prefix: "## ", type: "h2" },
|
|
27
|
+
{ prefix: "# ", type: "h1" },
|
|
28
|
+
]
|
|
29
|
+
const COMPONENTS = {
|
|
30
|
+
h1: H1,
|
|
31
|
+
h2: H2,
|
|
32
|
+
h3: H3,
|
|
33
|
+
h4: H4,
|
|
34
|
+
h5: H5,
|
|
35
|
+
h6: H6,
|
|
36
|
+
blockquote: Blockquote,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function format(text) {
|
|
40
|
+
if (!text.includes("*") && !text.includes("`")) {
|
|
41
|
+
return text
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = []
|
|
45
|
+
let i = 0
|
|
46
|
+
while (i < text.length) {
|
|
47
|
+
if (text[i] === "`") {
|
|
48
|
+
const start = i + 1
|
|
49
|
+
const end = text.indexOf("`", start)
|
|
50
|
+
if (end === -1) {
|
|
51
|
+
result.push(text[i])
|
|
52
|
+
i++
|
|
53
|
+
} else {
|
|
54
|
+
result.push(Code({}, text.substring(start, end)))
|
|
55
|
+
i = end + 1
|
|
56
|
+
}
|
|
57
|
+
} else if (text[i] === "*" && text[i + 1] === "*") {
|
|
58
|
+
const start = i + 2
|
|
59
|
+
const end = text.indexOf("**", start)
|
|
60
|
+
if (end === -1) {
|
|
61
|
+
result.push(text[i])
|
|
62
|
+
i++
|
|
63
|
+
} else {
|
|
64
|
+
result.push(Strong(text.substring(start, end)))
|
|
65
|
+
i = end + 2
|
|
66
|
+
}
|
|
67
|
+
} else if (text[i] === "*") {
|
|
68
|
+
const start = i + 1
|
|
69
|
+
const end = text.indexOf("*", start)
|
|
70
|
+
if (end === -1) {
|
|
71
|
+
result.push(text[i])
|
|
72
|
+
i++
|
|
73
|
+
} else {
|
|
74
|
+
result.push(Em(text.substring(start, end)))
|
|
75
|
+
i = end + 1
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
let next = text.length
|
|
79
|
+
const nextCode = text.indexOf("`", i)
|
|
80
|
+
const nextStar = text.indexOf("*", i)
|
|
81
|
+
if (nextCode !== -1 && nextCode < next) next = nextCode
|
|
82
|
+
if (nextStar !== -1 && nextStar < next) next = nextStar
|
|
83
|
+
if (next === text.length) {
|
|
84
|
+
result.push(text.substring(i))
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
result.push(text.substring(i, next))
|
|
88
|
+
i = next
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result.length > 0 ? result : text
|
|
92
|
+
}
|
|
2
93
|
|
|
3
94
|
function Markdown(params, children) {
|
|
4
|
-
if (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
.filter(Boolean)
|
|
10
|
-
|
|
11
|
-
return lines.map((line) => {
|
|
12
|
-
if (line.startsWith("# ")) {
|
|
13
|
-
const text = line.substring(2)
|
|
14
|
-
return H1(params, text)
|
|
15
|
-
} else if (line.startsWith("## ")) {
|
|
16
|
-
const text = line.substring(3)
|
|
17
|
-
return H2(params, text)
|
|
18
|
-
} else if (line.startsWith("### ")) {
|
|
19
|
-
const text = line.substring(4)
|
|
20
|
-
return H3(params, text)
|
|
21
|
-
} else if (line.startsWith("#### ")) {
|
|
22
|
-
const text = line.substring(5)
|
|
23
|
-
return H4(params, text)
|
|
24
|
-
} else if (line.startsWith("##### ")) {
|
|
25
|
-
const text = line.substring(6)
|
|
26
|
-
return H5(params, text)
|
|
27
|
-
} else if (line.startsWith("###### ")) {
|
|
28
|
-
const text = line.substring(7)
|
|
29
|
-
return H6(params, text)
|
|
30
|
-
} else if (line.startsWith("> ")) {
|
|
31
|
-
const text = line.substring(2)
|
|
32
|
-
return Blockquote(params, text)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return P(params, line)
|
|
36
|
-
})
|
|
37
|
-
} else {
|
|
95
|
+
if (Array.isArray(children)) {
|
|
96
|
+
return children.map((child) => Markdown(params, child))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof children !== "string") {
|
|
38
100
|
return null
|
|
39
101
|
}
|
|
102
|
+
|
|
103
|
+
const lines = children
|
|
104
|
+
.trim()
|
|
105
|
+
.split("\n")
|
|
106
|
+
.map((line) => line.trim())
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
|
|
109
|
+
const items = lines
|
|
110
|
+
.map((line) => {
|
|
111
|
+
if (UNORDERED_MARKERS.some((marker) => line.startsWith(marker))) {
|
|
112
|
+
const content = line.substring(2)
|
|
113
|
+
if (!content) return null
|
|
114
|
+
return { type: "li", list: "ul", content }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (ORDERED_LIST_REGEXP.test(line)) {
|
|
118
|
+
const content = line.replace(ORDERED_LIST_REGEXP, "")
|
|
119
|
+
if (!content) return null
|
|
120
|
+
return { type: "li", list: "ol", content }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const { prefix, type } of HEADINGS) {
|
|
124
|
+
if (line.startsWith(prefix)) {
|
|
125
|
+
return { type, content: line.substring(prefix.length) }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (line.startsWith("> ")) {
|
|
130
|
+
return { type: "blockquote", content: line.substring(2) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { type: "p", content: line }
|
|
134
|
+
})
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
|
|
137
|
+
const nodes = []
|
|
138
|
+
let i = 0
|
|
139
|
+
|
|
140
|
+
while (i < items.length) {
|
|
141
|
+
const item = items[i]
|
|
142
|
+
|
|
143
|
+
if (item.type === "li") {
|
|
144
|
+
const list = []
|
|
145
|
+
const parent = item.list
|
|
146
|
+
|
|
147
|
+
while (
|
|
148
|
+
i < items.length &&
|
|
149
|
+
items[i].type === "li" &&
|
|
150
|
+
items[i].list === parent
|
|
151
|
+
) {
|
|
152
|
+
list.push(Li(params, format(items[i].content)))
|
|
153
|
+
i++
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parent === "ul") {
|
|
157
|
+
nodes.push(Ul(params, list))
|
|
158
|
+
} else if (parent === "ol") {
|
|
159
|
+
nodes.push(Ol(params, list))
|
|
160
|
+
}
|
|
161
|
+
} else if (item.type === "blockquote") {
|
|
162
|
+
const lines = []
|
|
163
|
+
|
|
164
|
+
while (i < items.length && items[i].type === "blockquote") {
|
|
165
|
+
lines.push(items[i].content)
|
|
166
|
+
i++
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
nodes.push(Blockquote(params, P(params, format(lines.join("\n")))))
|
|
170
|
+
} else {
|
|
171
|
+
const { type, content } = item
|
|
172
|
+
const Component = COMPONENTS[type] || P
|
|
173
|
+
nodes.push(Component(params, format(content)))
|
|
174
|
+
i++
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return nodes
|
|
40
179
|
}
|
|
41
180
|
|
|
42
181
|
module.exports = component(Markdown)
|