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.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 Error(`FileError: symlinks are not allowed ("${current}")`)
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 Error(`FileError: path "${path}" has no extension`)
258
+ throw new FileError(`path "${path}" has no extension`)
195
259
  }
196
260
 
197
261
  if (!ALLOWED_READ_EXTENSIONS.includes(type)) {
198
- throw new Error(
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 Error(`FileError: path "${path}" is not a file`)
267
+ throw new FileError(`path "${path}" is not a file`)
206
268
  }
207
269
 
208
270
  if (stats.isSymbolicLink()) {
209
- throw new Error(`FileError: path "${path}" is a symbolic link`)
271
+ throw new FileError(`path "${path}" is a symbolic link`)
210
272
  }
211
273
 
212
274
  if (normalizedPath === normalizedBase) {
213
- throw new Error(
214
- `FileError: path "${path}" is the same as the current working directory "${base}"`
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 Error(
220
- `FileError: real path "${realPath}" is not within the current working directory "${realBase}"`
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 Error(
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 Error(
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 = (a, b, c) => {
499
- if (typeof b === "string" || typeof b === "number" || Array.isArray(b)) {
500
- const name = a
501
- const children = b
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: children,
587
+ name: tagName,
588
+ children: attrsOrChildren,
505
589
  }
506
590
  }
507
- const name = a
508
- const attributes = b
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 occurences(input, string) {
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 = occurences(content, character1)
610
- const count2 = occurences(content, character2)
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 Error(`CSSError: invalid CSS for path "${file}": ${message}`)
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 = (name) => (options, children) => tag(name, options, children)
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 Error(
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 Error(
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 Error(
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 Error(`TranslationError: language is undefined`)
1094
+ throw new TranslationError(`language is undefined`)
999
1095
  }
1000
1096
  if (!key) {
1001
- throw new Error(`TranslationError: key is undefined`)
1097
+ throw new TranslationError(`key is undefined`)
1002
1098
  }
1003
1099
  if (!data[key] || !data[key][language]) {
1004
- throw new Error(
1005
- `TranslationError: translation [${key}][${language}] is undefined`
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 Error(
1020
- `TranslationError: language is undefined for component:\n${fn.toString()}`
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 Error(
1027
- `TranslationError: key is undefined for component:\n${fn.toString()}`
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 Error(
1032
- `TranslationError: translation [${key}][${language}] is undefined for component:\n${fn.toString()}`
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.6.0",
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": "^10.1.3",
50
+ "c8": "^11.0.0",
51
51
  "express": "^5.2.1",
52
52
  "handlebars": "^4.7.8",
53
- "jsdom": "^27.3.0",
53
+ "jsdom": "^28.1.0",
54
54
  "mustache": "^4.2.0",
55
- "underscore": "^1.13.7"
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.0"
65
+ "css-tree": "^3.2.1"
66
66
  },
67
67
  "prettier": {
68
68
  "semi": false
@@ -1,42 +1,181 @@
1
- const { component, H1, H2, H3, H4, H5, H6, P, Blockquote } = require("../..")
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 (typeof children === "string") {
5
- const lines = children
6
- .trim()
7
- .split("\n")
8
- .map((line) => line.trim())
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)