diff --git a/package-lock.json b/package-lock.json
index 31295f14a..79f4e34f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,11 +10,11 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.24.4",
+ "@babel/core": "^7.24.5",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
- "@googleapis/drive": "^8.7.0",
+ "@googleapis/drive": "^8.8.0",
"body-parser": "^1.20.2",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
@@ -31,6 +31,7 @@
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "11.2.0",
+ "marked-emoji": "^1.4.0",
"marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.1.3",
"marked-smartypants-lite": "^1.0.2",
@@ -39,17 +40,17 @@
"mongoose": "^8.3.3",
"nanoid": "3.3.4",
"nconf": "^0.12.1",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.23.0",
"sanitize-filename": "1.6.3",
- "superagent": "^9.0.1",
+ "superagent": "^9.0.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.57.0",
- "eslint-plugin-jest": "^28.2.0",
+ "eslint-plugin-jest": "^28.5.0",
"eslint-plugin-react": "^7.34.1",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
@@ -107,20 +108,20 @@
}
},
"node_modules/@babel/core": {
- "version": "7.24.4",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz",
- "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz",
+ "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.2",
- "@babel/generator": "^7.24.4",
+ "@babel/generator": "^7.24.5",
"@babel/helper-compilation-targets": "^7.23.6",
- "@babel/helper-module-transforms": "^7.23.3",
- "@babel/helpers": "^7.24.4",
- "@babel/parser": "^7.24.4",
+ "@babel/helper-module-transforms": "^7.24.5",
+ "@babel/helpers": "^7.24.5",
+ "@babel/parser": "^7.24.5",
"@babel/template": "^7.24.0",
- "@babel/traverse": "^7.24.1",
- "@babel/types": "^7.24.0",
+ "@babel/traverse": "^7.24.5",
+ "@babel/types": "^7.24.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -141,11 +142,11 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"node_modules/@babel/generator": {
- "version": "7.24.4",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz",
- "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz",
+ "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==",
"dependencies": {
- "@babel/types": "^7.24.0",
+ "@babel/types": "^7.24.5",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
@@ -311,15 +312,15 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
- "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz",
+ "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
- "@babel/helper-module-imports": "^7.22.15",
- "@babel/helper-simple-access": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/helper-validator-identifier": "^7.22.20"
+ "@babel/helper-module-imports": "^7.24.3",
+ "@babel/helper-simple-access": "^7.24.5",
+ "@babel/helper-split-export-declaration": "^7.24.5",
+ "@babel/helper-validator-identifier": "^7.24.5"
},
"engines": {
"node": ">=6.9.0"
@@ -380,11 +381,11 @@
}
},
"node_modules/@babel/helper-simple-access": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
- "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz",
+ "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==",
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/types": "^7.24.5"
},
"engines": {
"node": ">=6.9.0"
@@ -450,13 +451,13 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.24.4",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz",
- "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz",
+ "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==",
"dependencies": {
"@babel/template": "^7.24.0",
- "@babel/traverse": "^7.24.1",
- "@babel/types": "^7.24.0"
+ "@babel/traverse": "^7.24.5",
+ "@babel/types": "^7.24.5"
},
"engines": {
"node": ">=6.9.0"
@@ -477,9 +478,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.24.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
- "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
+ "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1775,18 +1776,18 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
- "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz",
+ "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==",
"dependencies": {
- "@babel/code-frame": "^7.24.1",
- "@babel/generator": "^7.24.1",
+ "@babel/code-frame": "^7.24.2",
+ "@babel/generator": "^7.24.5",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.24.1",
- "@babel/types": "^7.24.0",
+ "@babel/helper-split-export-declaration": "^7.24.5",
+ "@babel/parser": "^7.24.5",
+ "@babel/types": "^7.24.5",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -1983,9 +1984,9 @@
}
},
"node_modules/@googleapis/drive": {
- "version": "8.7.0",
- "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.7.0.tgz",
- "integrity": "sha512-XAi6kfySIU4H3ivX2DpzTDce5UhNke5NxEWCL6tySEdcVqx+cmXJmkMqwfOAHJalEB5s9PPfdLBU29Xd5XlLSQ==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.8.0.tgz",
+ "integrity": "sha512-EOZ9GZCOUdej9PJVnkai7qu5RPyFLYse8FlpgijzfnZPOACXWFf4XOFuAuMcMw4Zue8xPhAPHu1qYcy8u362Xw==",
"dependencies": {
"googleapis-common": "^7.0.0"
},
@@ -5748,12 +5749,12 @@
}
},
"node_modules/eslint-plugin-jest": {
- "version": "28.2.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.2.0.tgz",
- "integrity": "sha512-yRDti/a+f+SMSmNTiT9/M/MzXGkitl8CfzUxnpoQcTyfq8gUrXMriVcWU36W1X6BZSUoyUCJrDAWWUA2N4hE5g==",
+ "version": "28.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.5.0.tgz",
+ "integrity": "sha512-6np6DGdmNq/eBbA7HOUNV8fkfL86PYwBfwyb8n23FXgJNTR8+ot3smRHjza9LGsBBZRypK3qyF79vMjohIL8eQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/utils": "^6.0.0"
+ "@typescript-eslint/utils": "^6.0.0 || ^7.0.0"
},
"engines": {
"node": "^16.10.0 || ^18.12.0 || >=20.0.0"
@@ -10144,6 +10145,14 @@
"node": ">= 18"
}
},
+ "node_modules/marked-emoji": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-1.4.0.tgz",
+ "integrity": "sha512-/2TJfGzXpiBBq+X3akHHbTrAjZPJDwR+7FV6SyQLECnQEfaoVkrpKZJzHhPTAq3Sl/A1l2frMT0u6b38VBBlNg==",
+ "peerDependencies": {
+ "marked": ">=4 <13"
+ }
+ },
"node_modules/marked-extended-tables": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/marked-extended-tables/-/marked-extended-tables-1.0.8.tgz",
@@ -11936,9 +11945,9 @@
}
},
"node_modules/react": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
- "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -11947,15 +11956,15 @@
}
},
"node_modules/react-dom": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
- "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
- "scheduler": "^0.23.0"
+ "scheduler": "^0.23.2"
},
"peerDependencies": {
- "react": "^18.2.0"
+ "react": "^18.3.1"
}
},
"node_modules/react-frame-component": {
@@ -12522,9 +12531,9 @@
}
},
"node_modules/scheduler": {
- "version": "0.23.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
- "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -13597,9 +13606,9 @@
}
},
"node_modules/superagent": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.1.tgz",
- "integrity": "sha512-CcRSdb/P2oUVaEpQ87w9Obsl+E9FruRd6b2b7LdiBtJoyMr2DQt7a89anAfiX/EL59j9b2CbRFvf2S91DhuCww==",
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz",
+ "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==",
"dependencies": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.4",
@@ -13609,24 +13618,12 @@
"formidable": "^3.5.1",
"methods": "^1.1.2",
"mime": "2.6.0",
- "qs": "^6.11.0",
- "semver": "^7.3.8"
+ "qs": "^6.11.0"
},
"engines": {
"node": ">=14.18.0"
}
},
- "node_modules/superagent/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/superagent/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
@@ -13638,25 +13635,6 @@
"node": ">=4.0.0"
}
},
- "node_modules/superagent/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/superagent/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
"node_modules/supertest": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz",
diff --git a/package.json b/package.json
index df7f700ef..e916467e6 100644
--- a/package.json
+++ b/package.json
@@ -27,11 +27,11 @@
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:variables": "jest tests/markdown/variables.test.js --verbose",
- "test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
- "test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
- "test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
- "test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
- "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
+ "test:mustache-syntax": "jest \".*(mustache-syntax).*\" --verbose --noStackTrace",
+ "test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
+ "test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
+ "test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
+ "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build",
@@ -81,11 +81,11 @@
]
},
"dependencies": {
- "@babel/core": "^7.24.4",
+ "@babel/core": "^7.24.5",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
- "@googleapis/drive": "^8.7.0",
+ "@googleapis/drive": "^8.8.0",
"body-parser": "^1.20.2",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
@@ -102,6 +102,7 @@
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "11.2.0",
+ "marked-emoji": "^1.4.0",
"marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.1.3",
"marked-smartypants-lite": "^1.0.2",
@@ -110,17 +111,17 @@
"mongoose": "^8.3.3",
"nanoid": "3.3.4",
"nconf": "^0.12.1",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.23.0",
"sanitize-filename": "1.6.3",
- "superagent": "^9.0.1",
+ "superagent": "^9.0.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.57.0",
- "eslint-plugin-jest": "^28.2.0",
+ "eslint-plugin-jest": "^28.5.0",
"eslint-plugin-react": "^7.34.1",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js
index f72955bf3..1c44c57ca 100644
--- a/shared/naturalcrit/markdown.js
+++ b/shared/naturalcrit/markdown.js
@@ -50,7 +50,7 @@ renderer.html = function (html) {
return html;
};
-// Don't wrap {{ Divs or {{ empty Spans in
tags
+// Don't wrap {{ Spans alone on a line, or {{ Divs in
tags
renderer.paragraph = function(text){
let match;
if(text.startsWith('
`${key}="${value}"`).join(' ')}` : ''}` +
+ `>${this.parser.parseInline(token.tokens)}`; // parseInline to turn child tokens into HTML
}
};
@@ -149,13 +156,13 @@ const mustacheDivs = {
if(match) {
//Find closing delimiter
let blockCount = 0;
- let tags = '';
+ let tags = {};
let endTags = 0;
let endToken = 0;
let delim;
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
- if(!tags) {
- tags = `${processStyleTags(delim.substring(2))}`;
+ if(_.isEmpty(tags)) {
+ tags = processStyleTags(delim.substring(2));
endTags = delim.length + src.indexOf(delim);
}
if(delim.startsWith('{{')) {
@@ -183,7 +190,14 @@ const mustacheDivs = {
}
},
renderer(token) {
- return `
`${key}="${value}"`).join(' ')}` : ''}` +
+ `>${this.parser.parse(token.tokens)}
`; // parse to turn child tokens into HTML
}
};
@@ -199,23 +213,39 @@ const mustacheInjectInline = {
if(!lastToken || lastToken.type == 'mustacheInjectInline')
return false;
- const tags = `${processStyleTags(match[1])}`;
+ const tags = processStyleTags(match[1]);
lastToken.originalType = lastToken.type;
lastToken.type = 'mustacheInjectInline';
- lastToken.tags = tags;
+ lastToken.injectedTags = tags;
return {
- type : 'text', // Should match "name" above
+ type : 'mustacheInjectInline', // Should match "name" above
raw : match[0], // Text to consume from the source
text : ''
};
}
},
renderer(token) {
+ if(!token.originalType){
+ return;
+ }
token.type = token.originalType;
const text = this.parser.parseInline([token]);
- const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
+ const originalTags = extractHTMLStyleTags(text);
+ const injectedTags = token.injectedTags;
+ const tags = {
+ id : injectedTags.id || originalTags.id || null,
+ classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
+ styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
+ attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
+ };
+ const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
if(openingTag) {
- return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
+ return `${openingTag[1]}` +
+ `${tags.classes ? ` class="${tags.classes}"` : ''}` +
+ `${tags.id ? ` id="${tags.id}"` : ''}` +
+ `${tags.styles ? ` style="${tags.styles}"` : ''}` +
+ `${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
+ `${openingTag[2]}`; // parse to turn child tokens into HTML
}
return text;
}
@@ -235,7 +265,7 @@ const mustacheInjectBlock = {
return false;
lastToken.originalType = 'mustacheInjectBlock';
- lastToken.tags = `${processStyleTags(match[1])}`;
+ lastToken.injectedTags = processStyleTags(match[1]);
return {
type : 'mustacheInjectBlock', // Should match "name" above
raw : match[0], // Text to consume from the source
@@ -249,9 +279,22 @@ const mustacheInjectBlock = {
}
token.type = token.originalType;
const text = this.parser.parse([token]);
- const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
+ const originalTags = extractHTMLStyleTags(text);
+ const injectedTags = token.injectedTags;
+ const tags = {
+ id : injectedTags.id || originalTags.id || null,
+ classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
+ styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
+ attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
+ };
+ const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
if(openingTag) {
- return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
+ return `${openingTag[1]}` +
+ `${tags.classes ? ` class="${tags.classes}"` : ''}` +
+ `${tags.id ? ` id="${tags.id}"` : ''}` +
+ `${tags.styles ? ` style="${tags.styles}"` : ''}` +
+ `${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
+ `${openingTag[2]}`; // parse to turn child tokens into HTML
}
return text;
}
@@ -687,15 +730,45 @@ const processStyleTags = (string)=>{
//TODO: can we simplify to just split on commas?
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
- const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
- const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('=')));
- const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'));
- const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : [];
+ const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null;
+ const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null;
+ const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'))
+ ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
+ .reduce((obj, attr) => {
+ let [key, value] = attr.split("=");
+ value = value.replace(/"/g, '');
+ obj[key] = value;
+ return obj;
+ }, {}) || null;
+ const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null;
- return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` +
- `${id ? ` id="${id}"` : ''}` +
- `${styles?.length ? ` style="${styles.join(' ')}"` : ''}` +
- `${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
+ return {
+ id : id,
+ classes : classes,
+ styles : styles,
+ attributes : _.isEmpty(attributes) ? null : attributes
+ };
+};
+
+const extractHTMLStyleTags = (htmlString)=> {
+ const id = htmlString.match(/id="([^"]*)"/)?.[1] || null;
+ const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null;
+ const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null;
+ const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g)
+ ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
+ .reduce((obj, attr) => {
+ let [key, value] = attr.split("=");
+ value = value.replace(/"/g, '');
+ obj[key] = value;
+ return obj;
+ }, {}) || null;
+
+ return {
+ id : id,
+ classes : classes,
+ styles : styles,
+ attributes : _.isEmpty(attributes) ? null : attributes
+ };
};
const globalVarsList = {};
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index 835bcc575..b32876353 100644
--- a/tests/markdown/mustache-syntax.test.js
+++ b/tests/markdown/mustache-syntax.test.js
@@ -130,8 +130,8 @@ describe('Inline: When using the Inline syntax {{ }}', ()=>{
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it('Renders a div with text only', function() {
const source = dedent`{{
- text
- }}`;
+ text
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
@@ -139,14 +139,14 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it('Renders an empty div', function() {
const source = dedent`{{
- }}`;
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a single paragraph with opening and closing brackets', function() {
const source = dedent`{{
- }}`;
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
{{}}
`);
});
@@ -154,79 +154,79 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it('Renders a div with a single class', function() {
const source = dedent`{{cat
- }}`;
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with a single class and text', function() {
const source = dedent`{{cat
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with two classes and text', function() {
const source = dedent`{{cat,dog
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with a style and text', function() {
const source = dedent`{{color:red
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with a style that has a string variable, and text', function() {
const source = dedent`{{--stringVariable:"'string'"
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with a style that has a string variable, and text', function() {
const source = dedent`{{--stringVariable:"'string'"
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with a class, style and text', function() {
const source = dedent`{{cat,color:red
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with an ID, class, style and text (different order)', function() {
const source = dedent`{{color:red,cat,#dog
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with a single ID', function() {
const source = dedent`{{#cat,#dog
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
it('Renders a div with an ID, class, style and text, and a variable assignment', function() {
const source = dedent`{{color:red,cat,#dog,a="b and c",d="e"
- Sample text.
- }}`;
+ Sample text.
+ }}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
@@ -243,61 +243,91 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
describe('Injection: When an injection tag follows an element', ()=>{
// FIXME: Most of these fail because injections currently replace attributes, rather than append to. Or just minor extra whitespace issues.
describe('and that element is an inline-block', ()=>{
- it.failing('Renders a span "text" with no injection', function() {
+ it('Renders a span "text" with no injection', function() {
const source = '{{ text}}{}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
});
- it.failing('Renders a span "text" with injected Class name', function() {
+ it('Renders a span "text" with injected Class name', function() {
const source = '{{ text}}{ClassName}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
});
- it.failing('Renders a span "text" with injected attribute', function() {
+ it('Renders a span "text" with injected attribute', function() {
const source = '{{ text}}{a="b and c"}';
const rendered = Markdown.render(source);
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
});
- it.failing('Renders a span "text" with injected style', function() {
+ it('Renders a span "text" with injected style', function() {
const source = '{{ text}}{color:red}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
});
- it.failing('Renders a span "text" with injected style using a string variable', function() {
+ it('Renders a span "text" with injected style using a string variable', function() {
const source = `{{ text}}{--stringVariable:"'string'"}`;
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
text`);
});
- it.failing('Renders a span "text" with two injected styles', function() {
+ it('Renders a span "text" with two injected styles', function() {
const source = '{{ text}}{color:red,background:blue}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
});
- it.failing('Renders an emphasis element with injected Class name', function() {
+ it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
+ const source = '{{#oldId text}}{#newId}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
+ const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new"}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
+ const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new",class="new",style="new",id="new"}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
+ });
+
+ it('Renders a span "text" with its own styles, appended with injected styles', function() {
+ const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
+ });
+
+ it('Renders a span "text" with its own classes, appended with injected classes', function() {
+ const source = '{{classA,classB text}}{classA,classC}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text');
+ });
+
+ it('Renders an emphasis element with injected Class name', function() {
const source = '*emphasis*{big}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
emphasis
');
});
- it.failing('Renders a code element with injected style', function() {
+ it('Renders a code element with injected style', function() {
const source = '`code`{background:gray}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
code
');
});
- it.failing('Renders an image element with injected style', function() {
+ it('Renders an image element with injected style', function() {
const source = '{position:absolute}';
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

');
});
- it.failing('Renders an element modified by only the first of two consecutive injections', function() {
+ it('Renders an element modified by only the first of two consecutive injections', function() {
const source = '{{ text}}{color:red}{background:blue}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text{background:blue}
');
@@ -306,61 +336,106 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image with added attributes', function() {
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

`);
});
});
describe('and that element is a block', ()=>{
- it.failing('renders a div "text" with no injection', function() {
+ it('renders a div "text" with no injection', function() {
const source = '{{\ntext\n}}\n{}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
});
- it.failing('renders a div "text" with injected Class name', function() {
+ it('renders a div "text" with injected Class name', function() {
const source = '{{\ntext\n}}\n{ClassName}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
});
- it.failing('renders a div "text" with injected style', function() {
+ it('renders a div "text" with injected style', function() {
const source = '{{\ntext\n}}\n{color:red}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
});
- it.failing('renders a div "text" with two injected styles', function() {
+ it('renders a div "text" with two injected styles', function() {
const source = dedent`{{
- text
- }}
- {color:red,background:blue}`;
+ text
+ }}
+ {color:red,background:blue}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
- it.failing('renders a div "text" with injected variable string', function() {
+ it('renders a div "text" with injected variable string', function() {
const source = dedent`{{
- text
- }}
- {--stringVariable:"'string'"}`;
+ text
+ }}
+ {--stringVariable:"'string'"}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
- it.failing('renders an h2 header "text" with injected class name', function() {
+ it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
+ const source = dedent`{{#oldId
+ text
+ }}
+ {#newId}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
+ const source = dedent`{{attrA="old",attrB="old"
+ text
+ }}
+ {attrA="new",attrC="new"}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
+ const source = dedent`{{attrA="old",attrB="old"
+ text
+ }}
+ {attrA="new",attrC="new",class="new",style="new",id="new"}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own styles, appended with injected styles', function() {
+ const source = dedent`{{color:blue,height:10px
+ text
+ }}
+ {width:10px,color:red}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own classes, appended with injected classes', function() {
+ const source = dedent`{{classA,classB
+ text
+ }}
+ {classA,classC}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('renders an h2 header "text" with injected class name', function() {
const source = dedent`## text
- {ClassName}`;
+ {ClassName}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text
');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text
');
});
- it.failing('renders a table with injected class name', function() {
+ it('renders a table with injected class name', function() {
const source = dedent`| Experience Points | Level |
- |:------------------|:-----:|
- | 0 | 1 |
- | 300 | 2 |
+ |:------------------|:-----:|
+ | 0 | 1 |
+ | 300 | 2 |
- {ClassName}`;
+ {ClassName}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
| Experience Points | Level |
|---|
| 0 | 1 |
| 300 | 2 |
`);
});
@@ -376,23 +451,23 @@ describe('Injection: When an injection tag follows an element', ()=>{
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into
? Currently injects into last -
// });
- it.failing('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
+ it('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
const source = dedent`## text
{ClassName}
{secondInjection}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text
{secondInjection}
');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text
{secondInjection}
');
});
- it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
+ it('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
const source = dedent`{{
- outer text
- {{
- inner text
- }}
- {innerDiv}
- }}
- {outerDiv}`;
+ outer text
+ {{
+ inner text
+ }}
+ {innerDiv}
+ }}
+ {outerDiv}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js
index c909dafec..e6018e19f 100644
--- a/tests/markdown/variables.test.js
+++ b/tests/markdown/variables.test.js
@@ -329,7 +329,7 @@ describe('Normal Links and Images', ()=>{
const source = `{width:100px}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
- 
`.trimReturns());
+ 
`.trimReturns());
});
it('Renders normal links', function() {