diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 07a1940fb..735eb9af4 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -47,6 +47,19 @@ const SharePage = createClass({ this.props.brew.shareId; }, + renderEditLink : function(){ + if(!this.props.brew.editId) return; + + let editLink = this.props.brew.editId; + if(this.props.brew.googleId && !this.props.brew.stubbed) { + editLink = this.props.brew.googleId + editLink; + } + + return + edit + ; + }, + render : function(){ return
@@ -64,13 +77,14 @@ const SharePage = createClass({ source - + view - + {this.renderEditLink()} + download - + clone to new diff --git a/package-lock.json b/package-lock.json index a9f7a7e90..69f4a01f3 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" }, @@ -5808,12 +5809,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" @@ -10231,6 +10232,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", @@ -12028,9 +12037,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" }, @@ -12039,15 +12048,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": { @@ -12614,9 +12623,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" } 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/server/app.js b/server/app.js index 1d67c5a71..e5b802c72 100644 --- a/server/app.js +++ b/server/app.js @@ -23,9 +23,9 @@ const { splitTextStyleAndMetadata } = require('../shared/helpers.js'); const sanitizeBrew = (brew, accessType)=>{ brew._id = undefined; brew.__v = undefined; - if(accessType !== 'edit'){ + if(accessType !== 'edit' && accessType !== 'shareAuthor') { brew.editId = undefined; - } + } return brew; }; @@ -314,7 +314,6 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{ //Share Page app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ const { brew } = req; - req.ogMeta = { ...defaultMetaTags, title : req.brew.title || 'Untitled Brew', description : req.brew.description || 'No description.', @@ -333,7 +332,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r await HomebrewModel.increaseView({ shareId: brew.shareId }); } }; - sanitizeBrew(req.brew, 'share'); + + brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share'); splitTextStyleAndMetadata(req.brew); return next(); })); 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(`

text

`); }); @@ -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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); 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(`

Sample text.

`); }); @@ -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 = '![alt text](http://i.imgur.com/hMna6G0.png){position:absolute}'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

homebrew mug

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

alt text

'); }); - 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 = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {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(`

homebrew mug

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

homebrew mug

`); }); }); 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('

text

'); }); - 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('

text

'); }); - 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('

text

'); }); - 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(`

text

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

text

`); }); - 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(`

text

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

text

`); }); - 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('

text

'); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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 PointsLevel
01
3002
`); }); @@ -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('

    outer text

    inner text

    '); }); 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 = `![alt text](url){width:100px}`; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` -

    alt text

    `.trimReturns()); +

    alt text

    `.trimReturns()); }); it('Renders normal links', function() { diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 25b784cfc..57a39e507 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -543,10 +543,8 @@ color : white; text-shadow : unset; text-transform : uppercase; - filter : drop-shadow(0 0 1.5px black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black); + -webkit-text-stroke: 0.2cm black; + paint-order:stroke; } h2 { font-family : 'NodestoCapsCondensed'; @@ -554,10 +552,8 @@ font-weight : normal; color : white; letter-spacing : 0.1cm; - filter : drop-shadow(0 0 1px black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black); + -webkit-text-stroke: 0.14cm black; + paint-order:stroke; } hr { position : relative; @@ -603,10 +599,8 @@ font-size : 0.496cm; color : white; text-align : center; - filter : drop-shadow(0 0 0.7px black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black) - drop-shadow(0 0 0 black) drop-shadow(0 0 0 black); + -webkit-text-stroke: 0.1cm black; + paint-order:stroke; } .logo { position : absolute;