diff --git a/changelog.md b/changelog.md index 9f4a07ab2..e86c2ea0f 100644 --- a/changelog.md +++ b/changelog.md @@ -75,11 +75,188 @@ pre { .page { padding-bottom: 1.5cm; } + +.varSyntaxTable th:first-of-type { + width:6cm; +} ``` ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). +### Wednesday 21/2/2024 - v3.11.0 +{{taskList + +##### Gazook89 + +* [x] Brew view count no longer increases when viewed by owner + +Fixes issue [#3037](https://github.com/naturalcrit/homebrewery/issues/3037) + +* [x] Small tweak to PHB H3 sizing + +Fixes issue [#2989](https://github.com/naturalcrit/homebrewery/issues/2989) + +* [x] Add **Fold/Unfold All** {{fas,fa-compress-alt}} / {{fas,fa-expand-alt}} buttons to editor bar + +Fixes issue [#2965](https://github.com/naturalcrit/homebrewery/issues/2965) + + +##### G-Ambatte + +* [x] Share link added to Editor Access error page + +Fixes issue [#3086](https://github.com/naturalcrit/homebrewery/issues/3086) + +* [x] Add Darkbrewery theme to Editor theme selector {{fas,fa-palette}} + +Fixes issue [#3034](https://github.com/naturalcrit/homebrewery/issues/3034) + +* [x] Fix Firefox prints with alternating blank pages + +Fixes issue [#3115](https://github.com/naturalcrit/homebrewery/issues/3115) + +* [x] Admin page working again + +Fixes issue [#2657](https://github.com/naturalcrit/homebrewery/issues/2657) + + +##### 5e-Cleric + +* [x] Fix indenting issue with Monster Blocks and italics in Class Feature + +Fixes issues [#527](https://github.com/naturalcrit/homebrewery/issues/527), +[#3247](https://github.com/naturalcrit/homebrewery/issues/3247) + +* [x] Allow CSS vars in curly syntax to be formatted as strings using single quotes + +`{{--customVar:"'a string'"}}` + +Fixes issue [#3066](https://github.com/naturalcrit/homebrewery/issues/3066) + +* [x] Add *Elderberry Inn* icons {{ei,action}} `{{ei,icon-name}}` + +Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171) + +* [x] New {{openSans **{{fas,fa-keyboard}} FONTS** }} snippets! + +Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171) + +* [x] New page now opens in a new tab + + +##### abquintic (new contributor!) + +* [x] Add ^super^ `^abc^` and ^^sub^^ `^^abc^^` syntax. + +Fixes issue [#2171](https://github.com/naturalcrit/homebrewery/issues/2171) + +* [x] Add HTML tag assignment to curly syntax `{{tag=value}}` + +Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488) + +* [x] {{openSans **Brew → Clone to New**}} now clones tags + +Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488) + +##### calculuschild + +* [x] Better error messages for "Out of Google Drive Storage" and "Not logged in to edit" + +Fixes issues [2510](https://github.com/naturalcrit/homebrewery/issues/2510), +[2975](https://github.com/naturalcrit/homebrewery/issues/2975) + +* [x] New Variables syntax. See below for details. +}} + +{{wide + +### Brew Variable Syntax + +You may already be familiar with `[link](url)` and `![image](url)` syntax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features: + +{{varSyntaxTable +| syntax | description | +|:-------|-------------| +| `[var]:content` | Assigns a variable (must start on a line by itself, and ends at the next blank line) | +| `[var](content)` | Assigns a variable and outputs it (can be inline) | +| `[var]` | Outputs the variable contents as a link, if formatted as a valid link | +| `![var]` | Outputs as an image, if formatted as a valid image | +| `$[var]` | Outputs as Markdown | +| `$[var1 + var2 - 2 * var3]` | Performs math operations and outputs result if all variables are valid numbers | +}} + +}} + +{{wide,margin-top:0,margin-bottom:0 +### Examples +}} + +{{wide,columns:2,margin-top:0,margin-bottom:0 + +``` +[first]: Bob + +[last]: Jones + +My name is $[first] $[last]. +``` + +\column + +[first]: Bob + +[last]: Jones + +My name is $[first] $[last]. + +}} + +{{wide,columns:2,margin-top:0,margin-bottom:0 + +``` +[myTable]: +| h1 | h2 | +|----|----| +| c1 | c2 | + +Here is my table: +$[myTable] +``` + +\column + +[myTable]: +| h1 | h2 | +|----|----| +| c1 | c2 | + +Here is my table: +$[myTable] +}} + +{{wide,columns:2,margin-top:0,margin-bottom:0 + +``` +There are $[TableNum] tables total. + +#### Table $[TableNum](1): Horses + +#### Table $[TableNum]($[TableNum + 1]): Cows +``` + +\column + +There are $[TableNum] tables in this document. *(note: final value of `$[TableNum]` gets hoisted up if available)* + + +#### Table $[TableNum](1): Horses + +#### Table $[TableNum]($[TableNum + 1]): Cows +}} + +\page + ### Friday 13/10/2023 - v3.10.0 {{taskList diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 9208a2b90..58dd59bee 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -89,15 +89,16 @@ const BrewRenderer = (props)=>{ })); }; - const shouldRender = (index)=>{ - if(!state.isMounted) return false; + const isInView = (index)=>{ + if(!state.isMounted) + return false; + + if(index == props.currentEditorPage) //Already rendered before this step + return false; if(Math.abs(index - state.viewablePageNumber) <= 3) return true; - if(index + 1 == props.currentEditorPage) - return true; - return false; }; @@ -138,7 +139,7 @@ const BrewRenderer = (props)=>{ return ; } else { cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) - const html = Markdown.render(cleanPageText); + const html = Markdown.render(cleanPageText, index); return ; } }; @@ -150,8 +151,11 @@ const BrewRenderer = (props)=>{ if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes renderedPages.length = 0; + // Render currently-edited page first so cross-page effects (variables, links) can propagate out first + renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage); + _.forEach(rawPages, (page, index)=>{ - if((shouldRender(index) || !renderedPages[index]) && typeof window !== 'undefined'){ + if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range } }); diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx index 3c706d6f7..5a870c108 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx @@ -25,13 +25,10 @@ const NotificationPopup = createClass({ return ( <>
  • - Broken default logo on CoverPage
    - If you have used the Cover Page snippet and notice the Naturalcrit - logo is showing as a broken image, this is due to some small tweaks - of this BETA feature. To fix the logo in your cover page, rename - the image link "/assets/naturalCritLogoRed.svg". Remember - that any snippet marked "BETA" may have a similar change in the - future as we encounter any bugs or reworks. + Don't store IMAGES in Google Drive
    + Google Drive is not an image service, and will block images from being used + in brews if they get more views than expected. Google has confirmed they won't fix + this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
  • diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx index bdbf269f9..e21f6e8a3 100644 --- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx +++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx @@ -134,7 +134,7 @@ const BrewItem = createClass({
    {brew.tags?.length ? <> -
    +
    {brew.tags.map((tag, idx)=>{ const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/); @@ -144,7 +144,11 @@ const BrewItem = createClass({ : <> } - {brew.authors?.join(', ')} + {brew.authors?.map((author, index)=>( + <> + {author} + {index < brew.authors.length - 1 && ', '} + ))}
    diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less index a8bc4473c..46a347b3e 100644 --- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less +++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less @@ -48,6 +48,10 @@ &>span{ margin-right : 12px; line-height : 1.5em; + + a { + color:inherit; + } } } .brewTags span { diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index bec60d6a8..d5af310b5 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -113,7 +113,7 @@ const EditPage = createClass({ brew : { ...prevState.brew, text: text }, isPending : true, htmlErrors : htmlErrors, - currentEditorPage : this.refs.editor.getCurrentPage() + currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 }), ()=>{if(this.state.autoSave) this.trySave();}); }, diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 9802517b1..3d3139e74 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -31,9 +31,10 @@ const HomePage = createClass({ }, getInitialState : function() { return { - brew : this.props.brew, - welcomeText : this.props.brew.text, - error : undefined + brew : this.props.brew, + welcomeText : this.props.brew.text, + error : undefined, + currentEditorPage : 0 }; }, handleSave : function(){ @@ -53,7 +54,8 @@ const HomePage = createClass({ }, handleTextChange : function(text){ this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text } + brew : { ...prevState.brew, text: text }, + currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 })); }, renderNavbar : function(){ @@ -85,7 +87,12 @@ const HomePage = createClass({ renderer={this.state.brew.renderer} showEditButtons={false} /> - +
    diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 470c90b89..9877651c2 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -38,11 +38,12 @@ const NewPage = createClass({ const brew = this.props.brew; return { - brew : brew, - isSaving : false, - saveGoogle : (global.account && global.account.googleId ? true : false), - error : null, - htmlErrors : Markdown.validate(brew.text) + brew : brew, + isSaving : false, + saveGoogle : (global.account && global.account.googleId ? true : false), + error : null, + htmlErrors : Markdown.validate(brew.text), + currentEditorPage : 0 }; }, @@ -104,8 +105,9 @@ const NewPage = createClass({ if(htmlErrors.length) htmlErrors = Markdown.validate(text); this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors + brew : { ...prevState.brew, text: text }, + htmlErrors : htmlErrors, + currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 })); localStorage.setItem(BREWKEY, text); }, @@ -220,7 +222,15 @@ const NewPage = createClass({ onMetaChange={this.handleMetaChange} renderer={this.state.brew.renderer} /> - +
    ; diff --git a/client/homebrew/pages/printPage/printPage.jsx b/client/homebrew/pages/printPage/printPage.jsx index 37376d4b2..083410804 100644 --- a/client/homebrew/pages/printPage/printPage.jsx +++ b/client/homebrew/pages/printPage/printPage.jsx @@ -21,7 +21,8 @@ const PrintPage = createClass({ brew : { text : '', style : '', - renderer : 'legacy' + renderer : 'legacy', + lang : '' } }; }, @@ -32,7 +33,8 @@ const PrintPage = createClass({ text : this.props.brew.text || '', style : this.props.brew.style || undefined, renderer : this.props.brew.renderer || 'legacy', - theme : this.props.brew.theme || '5ePHB' + theme : this.props.brew.theme || '5ePHB', + lang : this.props.brew.lang || 'en' } }; }, @@ -49,7 +51,8 @@ const PrintPage = createClass({ text : brewStorage, style : styleStorage, renderer : metaStorage?.renderer || 'legacy', - theme : metaStorage?.theme || '5ePHB' + theme : metaStorage?.theme || '5ePHB', + lang : metaStorage?.lang || 'en' } }; }); @@ -100,7 +103,7 @@ const PrintPage = createClass({ {/* Apply CSS from Style tab */} {this.renderStyle()} -
    +
    {this.renderPages()}
    ; diff --git a/package-lock.json b/package-lock.json index 00c5c0ec8..dd74f36c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,27 @@ { "name": "homebrewery", - "version": "3.10.0", + "version": "3.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebrewery", - "version": "3.10.0", + "version": "3.11.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.23.7", - "@babel/plugin-transform-runtime": "^7.23.7", - "@babel/preset-env": "^7.23.8", + "@babel/core": "^7.23.9", + "@babel/plugin-transform-runtime": "^7.23.9", + "@babel/preset-env": "^7.23.9", "@babel/preset-react": "^7.23.3", - "@googleapis/drive": "^8.6.0", + "@googleapis/drive": "^8.7.0", "body-parser": "^1.20.2", "classnames": "^2.3.2", "codemirror": "^5.65.6", "cookie-parser": "^1.4.6", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", + "expr-eval": "^2.0.2", "express": "^4.18.2", "express-async-handler": "^1.2.0", "express-static-gzip": "2.1.7", @@ -29,32 +30,32 @@ "jwt-simple": "^0.5.6", "less": "^3.13.1", "lodash": "^4.17.21", - "marked": "5.1.1", + "marked": "11.2.0", "marked-extended-tables": "^1.0.8", - "marked-gfm-heading-id": "^3.1.2", + "marked-gfm-heading-id": "^3.1.3", "marked-smartypants-lite": "^1.0.2", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^8.1.0", + "mongoose": "^8.1.3", "nanoid": "3.3.4", "nconf": "^0.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-frame-component": "^4.1.3", - "react-router-dom": "6.21.3", + "react-router-dom": "6.22.1", "sanitize-filename": "1.6.3", "superagent": "^8.1.2", "vitreum": "git+https://git@github.com/calculuschild/vitreum.git" }, "devDependencies": { "eslint": "^8.56.0", - "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-react": "^7.33.2", "jest": "^29.7.0", "jest-expect-message": "^1.1.3", "postcss-less": "^6.0.0", "stylelint": "^15.11.0", - "stylelint-config-recess-order": "^4.4.0", + "stylelint-config-recess-order": "^4.6.0", "stylelint-config-recommended": "^13.0.0", "stylelint-stylistic": "^0.4.3", "supertest": "^6.3.4" @@ -106,20 +107,20 @@ } }, "node_modules/@babel/core": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", - "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.7", - "@babel/parser": "^7.23.6", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -242,9 +243,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", - "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -449,13 +450,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.7.tgz", - "integrity": "sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" @@ -475,9 +476,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "bin": { "parser": "bin/babel-parser.js" }, @@ -814,9 +815,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", - "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", @@ -1150,9 +1151,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", @@ -1455,15 +1456,15 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", - "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", + "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "semver": "^6.3.1" }, "engines": { @@ -1604,9 +1605,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.8.tgz", - "integrity": "sha512-lFlpmkApLkEP6woIKprO6DO60RImpatTQKtz4sUcDjVcK8M8mQ4sZsuxaTMNOZf0sqAq/ReYW1ZBHnOQwKpLWA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", + "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", @@ -1635,7 +1636,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", "@babel/plugin-transform-block-scoping": "^7.23.4", @@ -1657,7 +1658,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", @@ -1683,9 +1684,9 @@ "@babel/plugin-transform-unicode-regex": "^7.23.3", "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1745,22 +1746,22 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", - "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", @@ -1768,8 +1769,8 @@ "@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.23.6", - "@babel/types": "^7.23.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1778,9 +1779,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -1966,9 +1967,9 @@ } }, "node_modules/@googleapis/drive": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.6.0.tgz", - "integrity": "sha512-Af3/5i6h7gbjHnwFuO9zMTpYOy2yhhfZlNciUEjb14L3ZdT1WNIDM038viIAb9ovFzkrIDqLSfUbFCgh1pywkw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.7.0.tgz", + "integrity": "sha512-XAi6kfySIU4H3ivX2DpzTDce5UhNke5NxEWCL6tySEdcVqx+cmXJmkMqwfOAHJalEB5s9PPfdLBU29Xd5XlLSQ==", "dependencies": { "googleapis-common": "^7.0.0" }, @@ -2837,9 +2838,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", - "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz", + "integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==", "engines": { "node": ">=14.0.0" } @@ -3734,12 +3735,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", - "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.4", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -3747,23 +3748,23 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", - "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4", - "core-js-compat": "^3.33.1" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", - "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5642,9 +5643,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.6.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", - "integrity": "sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==", + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -5653,7 +5654,7 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", "eslint": "^7.0.0 || ^8.0.0", "jest": "*" }, @@ -6036,6 +6037,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expr-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz", + "integrity": "sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==" + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -10041,9 +10047,9 @@ } }, "node_modules/marked": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-5.1.1.tgz", - "integrity": "sha512-bTmmGdEINWmOMDjnPWDxGPQ4qkDLeYorpYbEtFOXzOruTwUE671q4Guiuchn4N8h/v6NGd7916kXsm3Iz4iUSg==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", + "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", "bin": { "marked": "bin/marked.js" }, @@ -10060,14 +10066,14 @@ } }, "node_modules/marked-gfm-heading-id": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-3.1.2.tgz", - "integrity": "sha512-SdIZvhNxDgndFkDa2WRcFP4ahYm6k6hoHdTCN+fD7HRiI/R3Eimcw/Yl7ikQ+0KUuDpi75NnYQiThZnZsNr9Dg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-3.1.3.tgz", + "integrity": "sha512-A0cRU4PCueX/5m8VE4mT8uTQ36l3xMYRojz3Eqnk4BmUFZ0T+9Xhn2KvHcANP4qbhfOeuMrWJCTQbASIBR5xeg==", "dependencies": { "github-slugger": "^2.0.0" }, "peerDependencies": { - "marked": ">=4 <12" + "marked": ">=4 <13" } }, "node_modules/marked-smartypants-lite": { @@ -10453,9 +10459,9 @@ } }, "node_modules/mongoose": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.1.0.tgz", - "integrity": "sha512-kOA4Xnq2goqNpN9EmYElGNWfxA9H80fxcr7UdJKWi3UMflza0R7wpTihCpM67dE/0MNFljoa0sjQtlXVkkySAQ==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.1.3.tgz", + "integrity": "sha512-a5MajZSDJiQgy0iQcR+MIpFe7zehGJI4doJ6Dh1MvnGh8/HNNhr5pn07RPA86KCTjP2vuKdffpFmvXxcHiUOjw==", "dependencies": { "bson": "^6.2.0", "kareem": "2.5.1", @@ -11873,11 +11879,11 @@ "dev": true }, "node_modules/react-router": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", - "integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==", + "version": "6.22.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz", + "integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==", "dependencies": { - "@remix-run/router": "1.14.2" + "@remix-run/router": "1.15.1" }, "engines": { "node": ">=14.0.0" @@ -11887,12 +11893,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz", - "integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==", + "version": "6.22.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.1.tgz", + "integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==", "dependencies": { - "@remix-run/router": "1.14.2", - "react-router": "6.21.3" + "@remix-run/router": "1.15.1", + "react-router": "6.22.1" }, "engines": { "node": ">=14.0.0" @@ -13327,9 +13333,9 @@ } }, "node_modules/stylelint-config-recess-order": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-4.4.0.tgz", - "integrity": "sha512-Q99kvZyIM/aoPEV4dRDkzD3fZLzH0LXi+pawCf1r700uUeF/PHQ5PZXjwFUuGrWhOzd1N+cuVm+OUGsY2fRN5A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-4.6.0.tgz", + "integrity": "sha512-V76fhv3YtcNXh/hyAuAdSzi5FmcrG54Mp2AThJ3D/PTMTSYzUPd7GIhP6z9mTqnRhmkk6YTfcu/JWB8h+Yrcaw==", "dev": true, "dependencies": { "stylelint-order": "6.x" diff --git a/package.json b/package.json index 50251c118..3852ffe13 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "homebrewery", "description": "Create authentic looking D&D homebrews using only markdown", - "version": "3.10.0", + "version": "3.11.0", "engines": { - "npm": "^10.2.x", + "npm": "^10.2.x", "node": "^20.8.x" }, "repository": { @@ -26,6 +26,7 @@ "test:coverage": "jest --coverage --silent --runInBand", "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", @@ -79,17 +80,18 @@ ] }, "dependencies": { - "@babel/core": "^7.23.7", - "@babel/plugin-transform-runtime": "^7.23.7", - "@babel/preset-env": "^7.23.8", + "@babel/core": "^7.23.9", + "@babel/plugin-transform-runtime": "^7.23.9", + "@babel/preset-env": "^7.23.9", "@babel/preset-react": "^7.23.3", - "@googleapis/drive": "^8.6.0", + "@googleapis/drive": "^8.7.0", "body-parser": "^1.20.2", "classnames": "^2.3.2", "codemirror": "^5.65.6", "cookie-parser": "^1.4.6", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", + "expr-eval": "^2.0.2", "express": "^4.18.2", "express-async-handler": "^1.2.0", "express-static-gzip": "2.1.7", @@ -98,32 +100,32 @@ "jwt-simple": "^0.5.6", "less": "^3.13.1", "lodash": "^4.17.21", - "marked": "5.1.1", + "marked": "11.2.0", "marked-extended-tables": "^1.0.8", - "marked-gfm-heading-id": "^3.1.2", + "marked-gfm-heading-id": "^3.1.3", "marked-smartypants-lite": "^1.0.2", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^8.1.0", + "mongoose": "^8.1.3", "nanoid": "3.3.4", "nconf": "^0.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-frame-component": "^4.1.3", - "react-router-dom": "6.21.3", + "react-router-dom": "6.22.1", "sanitize-filename": "1.6.3", "superagent": "^8.1.2", "vitreum": "git+https://git@github.com/calculuschild/vitreum.git" }, "devDependencies": { "eslint": "^8.56.0", - "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-react": "^7.33.2", "jest": "^29.7.0", "jest-expect-message": "^1.1.3", "postcss-less": "^6.0.0", "stylelint": "^15.11.0", - "stylelint-config-recess-order": "^4.4.0", + "stylelint-config-recess-order": "^4.6.0", "stylelint-config-recommended": "^13.0.0", "stylelint-stylistic": "^0.4.3", "supertest": "^6.3.4" diff --git a/scripts/project.json b/scripts/project.json index 5a0289ad0..4c769660f 100644 --- a/scripts/project.json +++ b/scripts/project.json @@ -26,7 +26,6 @@ "codemirror/addon/edit/trailingspace.js", "codemirror/addon/selection/active-line.js", "moment", - "superagent", - "marked" + "superagent" ] } diff --git a/server/admin.api.js b/server/admin.api.js index b9b2afbd7..5363ecc08 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -26,85 +26,124 @@ const mw = { } }; - -/* Search for brews that are older than 3 days and that are shorter than a tweet */ -const junkBrewQuery = HomebrewModel.find({ - '$where' : 'this.text.length < 140', - createdAt : { - $lt : Moment().subtract(30, 'days').toDate() - } -}).limit(100).maxTime(60000); +const junkBrewPipeline = [ + { $match : { + updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, + lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } + }}, + { $project: { textBinSize: { $binarySize: '$textBin' } } }, + { $match: { textBinSize: { $lt: 140 } } }, + { $limit: 100 } +]; /* Search for brews that aren't compressed (missing the compressed text field) */ const uncompressedBrewQuery = HomebrewModel.find({ 'text' : { '$exists': true } }).lean().limit(10000).select('_id'); +// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{ - junkBrewQuery.exec((err, objs)=>{ - if(err) return res.status(500).send(err); - return res.json({ count: objs.length }); - }); + HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) + .then((objs)=>res.json({ count: objs.length })) + .catch((error)=>{ + console.error(error); + res.status(500).json({ error: 'Internal Server Error' }); + }); }); -/* Removes all empty brews that are older than 3 days and that are shorter than a tweet */ + +// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{ - junkBrewQuery.remove().exec((err, objs)=>{ - if(err) return res.status(500).send(err); - return res.json({ count: objs.length }); - }); + HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) + .then((docs)=>{ + const ids = docs.map((doc)=>doc._id); + return HomebrewModel.deleteMany({ _id: { $in: ids } }); + }).then((result)=>{ + res.json({ count: result.deletedCount }); + }).catch((error)=>{ + console.error(error); + res.status(500).json({ error: 'Internal Server Error' }); + }); }); /* Searches for matching edit or share id, also attempts to partial match */ -router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{ - HomebrewModel.findOne({ $or : [ - { editId: { '$regex': req.params.id, '$options': 'i' } }, - { shareId: { '$regex': req.params.id, '$options': 'i' } }, - ] }).exec((err, brew)=>{ - return res.json(brew); +router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{ + HomebrewModel.findOne({ + $or : [ + { editId: { $regex: req.params.id, $options: 'i' } }, + { shareId: { $regex: req.params.id, $options: 'i' } }, + ] + }).exec() + .then((brew)=>{ + if(!brew) // No document found + return res.status(404).json({ error: 'Document not found' }); + else + return res.json(brew); + }) + .catch((err)=>{ + console.error(err); + return res.status(500).json({ error: 'Internal Server Error' }); }); }); /* Find 50 brews that aren't compressed yet */ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{ - uncompressedBrewQuery.exec((err, objs)=>{ - if(err) return res.status(500).send(err); - objs = objs.map((obj)=>{return obj._id;}); - return res.json({ count: objs.length, ids: objs }); - }); + const query = uncompressedBrewQuery.clone(); + + query.exec() + .then((objs)=>{ + const ids = objs.map((obj)=>obj._id); + res.json({ count: ids.length, ids }); + }) + .catch((err)=>{ + console.error(err); + res.status(500).send(err.message || 'Internal Server Error'); + }); }); + /* Compresses the "text" field of a brew to binary */ router.put('/admin/compress/:id', (req, res)=>{ - HomebrewModel.get({ _id: req.params.id }) + HomebrewModel.findOne({ _id: req.params.id }) .then((brew)=>{ - brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving - brew.text = undefined; // Delete the non-binary text field since it's not needed anymore + if(!brew) + return res.status(404).send('Brew not found'); - brew.save((err, obj)=>{ - if(err) throw err; - return res.status(200).send(obj); - }); + if(brew.text) { + brew.textBin = brew.textBin || zlib.deflateRawSync(brew.text); //Don't overwrite textBin if exists + brew.text = undefined; + } + + return brew.save(); }) + .then((obj)=>res.status(200).send(obj)) .catch((err)=>{ - console.log(err); - return res.status(500).send('Error while saving'); + console.error(err); + res.status(500).send('Error while saving'); }); }); -router.get('/admin/stats', mw.adminOnly, (req, res)=>{ - HomebrewModel.count({}, (err, count)=>{ + +router.get('/admin/stats', mw.adminOnly, async (req, res)=>{ + try { + const totalBrewsCount = await HomebrewModel.countDocuments({}); + const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true }); + return res.json({ - totalBrews : count + totalBrews : totalBrewsCount, + totalPublishedBrews : publishedBrewsCount }); - }); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Internal Server Error' }); + } }); router.get('/admin', mw.adminOnly, (req, res)=>{ templateFn('admin', { url : req.originalUrl }) - .then((page)=>res.send(page)) - .catch((err)=>res.sendStatus(500)); + .then((page)=>res.send(page)) + .catch((err)=>res.sendStatus(500)); }); module.exports = router; diff --git a/server/app.js b/server/app.js index 970c2cd9c..fc5d4a035 100644 --- a/server/app.js +++ b/server/app.js @@ -304,7 +304,8 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{ text : req.brew.text, style : req.brew.style, renderer : req.brew.renderer, - theme : req.brew.theme + theme : req.brew.theme, + tags : req.brew.tags }; req.brew = _.defaults(brew, DEFAULT_BREW); diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 5be80ac97..09f810907 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -4,7 +4,40 @@ const Marked = require('marked'); const MarkedExtendedTables = require('marked-extended-tables'); const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite'); const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id'); +const MathParser = require('expr-eval').Parser; const renderer = new Marked.Renderer(); +const tokenizer = new Marked.Tokenizer(); + +//Limit math features to simple items +const mathParser = new MathParser({ + operators : { + // These default to true, but are included to be explicit + add : true, + subtract : true, + multiply : true, + divide : true, + power : true, + round : true, + floor : true, + ceil : true, + + sin : false, cos : false, tan : false, asin : false, acos : false, + atan : false, sinh : false, cosh : false, tanh : false, asinh : false, + acosh : false, atanh : false, sqrt : false, cbrt : false, log : false, + log2 : false, ln : false, lg : false, log10 : false, expm1 : false, + log1p : false, abs : false, trunc : false, join : false, sum : false, + '-' : false, '+' : false, exp : false, not : false, length : false, + '!' : false, sign : false, random : false, fac : false, min : false, + max : false, hypot : false, pyt : false, pow : false, atan2 : false, + 'if' : false, gamma : false, roundTo : false, map : false, fold : false, + filter : false, indexOf : false, + + remainder : false, factorial : false, + comparison : false, concatenate : false, + logical : false, assignment : false, + array : false, fndef : false + } +}); //Processes the markdown within an HTML block if it's just a class-wrapper renderer.html = function (html) { @@ -28,6 +61,33 @@ renderer.paragraph = function(text){ return `

    ${text}

    \n`; }; +//Fix local links in the Preview iFrame to link inside the frame +renderer.link = function (href, title, text) { + let self = false; + if(href[0] == '#') { + self = true; + } + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + + if(href === null) { + return text; + } + let out = `${text}`; + return out; +}; + +// Disable default reflink behavior, as it steps on our variables extension +tokenizer.def = function () { + return undefined; +}; + const mustacheSpans = { name : 'mustacheSpans', level : 'inline', // Is this a block-level or inline-level tokenizer? @@ -266,33 +326,257 @@ const definitionLists = { } }; + +//v=====--------------------< Variable Handling >-------------------=====v// 242 lines +const replaceVar = function(input, hoist=false, allowUnresolved=false) { + const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; + const match = regex.exec(input); + + const prefix = match[1]; + const label = match[2]; + + //v=====--------------------< HANDLE MATH >-------------------=====v// + const mathRegex = /[a-z]+\(|[+\-*/^()]/g; + const matches = label.split(mathRegex); + const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names + + let replacedLabel = label; + + if(prefix[0] == '$' && mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math! + mathVars?.forEach((variable)=>{ + const foundVar = lookupVar(variable, globalPageNumber, hoist); + if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers + replacedLabel = replacedLabel.replaceAll(variable, foundVar.content); + }); + + try { + return mathParser.evaluate(replacedLabel); + } catch (error) { + return undefined; // Return undefined if invalid math result + } + } + //^=====--------------------< HANDLE MATH >-------------------=====^// + + const foundVar = lookupVar(label, globalPageNumber, hoist); + + if(!foundVar || (!foundVar.resolved && !allowUnresolved)) + return undefined; // Return undefined if not found, or parially-resolved vars are not allowed + + // url or "title" or 'title' or (title) + const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m; + const linkMatch = linkRegex.exec(foundVar.content); + + const href = linkMatch ? linkMatch[1] : null; //TODO: TRIM OFF < > IF PRESENT + const title = linkMatch ? linkMatch[2]?.slice(1, -1) : null; + + if(!prefix[0] && href) // Link + return `[${label}](${href}${title ? ` "${title}"` : ''})`; + + if(prefix[0] == '!' && href) // Image + return `![${label}](${href} ${title ? ` "${title}"` : ''})`; + + if(prefix[0] == '$') // Variable + return foundVar.content; +}; + +const lookupVar = function(label, index, hoist=false) { + while (index >= 0) { + if(globalVarsList[index]?.[label] !== undefined) + return globalVarsList[index][label]; + index--; + } + + if(hoist) { //If normal lookup failed, attempt hoisting + index = Object.keys(globalVarsList).length; // Move index to start from last page + while (index >= 0) { + if(globalVarsList[index]?.[label] !== undefined) + return globalVarsList[index][label]; + index--; + } + } + + return undefined; +}; + +const processVariableQueue = function() { + let resolvedOne = true; + let finalLoop = false; + while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved + resolvedOne = false; + for (const item of varsQueue) { + if(item.type == 'text') + continue; + + if(item.type == 'varDefBlock') { + const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; + let match; + let resolved = true; + let tempContent = item.content; + while (match = regex.exec(item.content)) { // regex to find variable calls + const value = replaceVar(match[0], true); + + if(value == undefined) + resolved = false; + else + tempContent = tempContent.replaceAll(match[0], value); + } + + if(resolved == true || item.content != tempContent) { + resolvedOne = true; + item.content = tempContent; + } + + globalVarsList[globalPageNumber][item.varName] = { + content : item.content, + resolved : resolved + }; + + if(resolved) + item.type = 'resolved'; + } + + if(item.type == 'varCallBlock' || item.type == 'varCallInline') { + const value = replaceVar(item.content, true, finalLoop); // final loop will just use the best value so far + + if(value == undefined) + continue; + + resolvedOne = true; + item.content = value; + item.type = 'text'; + } + } + varsQueue = varsQueue.filter((item)=>item.type !== 'resolved'); // Remove any fully-resolved variable definitions + + if(finalLoop) + break; + if(!resolvedOne) + finalLoop = true; + } + varsQueue = varsQueue.filter((item)=>item.type !== 'varDefBlock'); +}; + +function MarkedVariables() { + return { + hooks : { + preprocess(src) { + const codeBlockSkip = /^(?: {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+|^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})(?:[^\n]*)(?:\n|$)(?:|(?:[\s\S]*?)(?:\n|$))(?: {0,3}\2[~`]* *(?=\n|$))|`[^`]*?`/; + const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 3, [4]:5 + const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 6, [7] + const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+)\)/; //Matches 8, 9[10](11) + const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 12, [13] + + // Combine regexes and wrap in parens like so: (regex1)|(regex2)|(regex3)|(regex4) + const combinedRegex = new RegExp([codeBlockSkip, blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map((s)=>`(${s.source})`).join('|'), 'gm'); + + let lastIndex = 0; + let match; + while ((match = combinedRegex.exec(src)) !== null) { + // Format any matches into tokens and store + if(match.index > lastIndex) { // Any non-variable stuff + varsQueue.push( + { type : 'text', + varName : null, + content : src.slice(lastIndex, match.index) + }); + } + if(match[1]) { + varsQueue.push( + { type : 'text', + varName : null, + content : match[0] + }); + } + if(match[3]) { // Block Definition + const label = match[4] ? match[4].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + varsQueue.push( + { type : 'varDefBlock', + varName : label, + content : content + }); + } + if(match[6]) { // Block Call + const label = match[7] ? match[7].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + varsQueue.push( + { type : 'varCallBlock', + varName : label, + content : match[0] + }); + } + if(match[8]) { // Inline Definition + const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + // In case of nested (), find the correct matching end ) + let level = 0; + let i; + for (i = 0; i < content.length; i++) { + if(content[i] === '\\') { + i++; + } else if(content[i] === '(') { + level++; + } else if(content[i] === ')') { + level--; + if(level < 0) + break; + } + } + if(i > -1) { + combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i); + content = content.slice(0, i).trim().replace(/\s+/g, ' '); + } + + varsQueue.push( + { type : 'varDefBlock', + varName : label, + content : content + }); + varsQueue.push( + { type : 'varCallInline', + varName : label, + content : match[9] + }); + } + if(match[12]) { // Inline Call + const label = match[13] ? match[13].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + varsQueue.push( + { type : 'varCallInline', + varName : label, + content : match[0] + }); + } + lastIndex = combinedRegex.lastIndex; + } + + if(lastIndex < src.length) { + varsQueue.push( + { type : 'text', + varName : null, + content : src.slice(lastIndex) + }); + } + + processVariableQueue(); + + const output = varsQueue.map((item)=>item.content).join(''); + varsQueue = []; // Must clear varsQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array + return output; + } + } + }; +}; +//^=====--------------------< Variable Handling >-------------------=====^// + +Marked.use(MarkedVariables()); Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] }); Marked.use(mustacheInjectBlock); -Marked.use({ renderer: renderer, mangle: false }); +Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite()); -//Fix local links in the Preview iFrame to link inside the frame -renderer.link = function (href, title, text) { - let self = false; - if(href[0] == '#') { - self = true; - } - href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); - - if(href === null) { - return text; - } - let out = `${text}`; - return out; -}; - const nonWordAndColonTest = /[^\w:]/g; const cleanUrl = function (sanitize, base, href) { if(sanitize) { @@ -369,12 +653,28 @@ const processStyleTags = (string)=>{ `${attributes?.length ? ` ${attributes.join(' ')}` : ''}`; }; +const globalVarsList = {}; +let varsQueue = []; +let globalPageNumber = 0; + module.exports = { marked : Marked, - render : (rawBrewText)=>{ + render : (rawBrewText, pageNumber=1)=>{ + globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order + varsQueue = []; //Could move into MarkedVariables() + globalPageNumber = pageNumber; + rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n
    \n`) .replace(/^(:+)$/gm, (match)=>`${`
    `.repeat(match.length)}\n`); - return Marked.parse(rawBrewText); + const opts = Marked.defaults; + + rawBrewText = opts.hooks.preprocess(rawBrewText); + const tokens = Marked.lexer(rawBrewText, opts); + + Marked.walkTokens(tokens, opts.walkTokens); + + const html = Marked.parser(tokens, opts); + return opts.hooks.postprocess(html); }, validate : (rawBrewText)=>{ diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js new file mode 100644 index 000000000..c909dafec --- /dev/null +++ b/tests/markdown/variables.test.js @@ -0,0 +1,373 @@ +/* eslint-disable max-lines */ + +const dedent = require('dedent-tabs').default; +const Markdown = require('naturalcrit/markdown.js'); + +// Marked.js adds line returns after closing tags on some default tokens. +// This removes those line returns for comparison sake. +String.prototype.trimReturns = function(){ + return this.replace(/\r?\n|\r/g, '').trim(); +}; + +renderAllPages = function(pages){ + const outputs = []; + pages.forEach((page, index)=>{ + const output = Markdown.render(page, index); + outputs.push(output); + }); + + return outputs; +}; + +// Adding `.failing()` method to `describe` or `it` will make failing tests "pass" as long as they continue to fail. +// Remove the `.failing()` method once you have fixed the issue. + +describe('Block-level variables', ()=>{ + it('Handles variable assignment and recall with simple text', function() { + const source = dedent` + [var]: string + + $[var] + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    string

    '); + }); + + it('Handles variable assignment and recall with multiline string', function() { + const source = dedent` + [var]: string + across multiple + lines + + $[var]`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    string across multiple lines

    '); + }); + + it('Handles variable assignment and recall with tables', function() { + const source = dedent` + [var]: + ##### Title + | H1 | H2 | + |:---|:--:| + | A | B | + | C | D | + + $[var]`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +
    Title
    + + + + + + +
    H1H2
    AB
    CD
    `.trimReturns()); + }); + + it('Hoists undefined variables', function() { + const source = dedent` + $[var] + + [var]: string`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    string

    '); + }); + + it('Hoists last instance of variable', function() { + const source = dedent` + $[var] + + [var]: string + + [var]: new string`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    new string

    '); + }); + + it('Handles complex hoisting', function() { + const source = dedent` + $[titleAndName]: $[title] $[fullName] + + $[title]: Mr. + + $[fullName]: $[firstName] $[lastName] + + [firstName]: Bob + + Welcome, $[titleAndName]! + + [lastName]: Jacob + + [lastName]: $[lastName]son + `; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    Welcome, Mr. Bob Jacobson!

    '); + }); + + it('Handles variable reassignment', function() { + const source = dedent` + [var]: one + + $[var] + + [var]: two + + $[var] + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    one

    two

    '.trimReturns()); + }); + + it('Handles variable reassignment with hoisting', function() { + const source = dedent` + $[var] + + [var]: one + + $[var] + + [var]: two + + $[var] + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    two

    one

    two

    '.trimReturns()); + }); + + it('Ignores undefined variables that can\'t be hoisted', function() { + const source = dedent` + $[var](My name is $[first] $[last]) + + $[last]: Jones + `; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

    My name is $[first] Jones

    `.trimReturns()); + }); +}); + +describe('Inline-level variables', ()=>{ + it('Handles variable assignment and recall with simple text', function() { + const source = dedent` + $[var](string) + + $[var] + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    string

    string

    '); + }); + + it('Hoists undefined variables when possible', function() { + const source = dedent` + $[var](My name is $[name] Jones) + + [name]: Bob`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    My name is Bob Jones

    '); + }); + + it('Hoists last instance of variable', function() { + const source = dedent` + $[var](My name is $[name] Jones) + + $[name](Bob) + + [name]: Bill`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

    My name is Bill Jones

    Bob

    `.trimReturns()); + }); + + it('Only captures nested parens if balanced', function() { + const source = dedent` + $[var1](A variable (with nested parens) inside) + + $[var1] + + $[var2](A variable ) with unbalanced parens) + + $[var2]`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    A variable (with nested parens) inside

    +

    A variable (with nested parens) inside

    +

    A variable with unbalanced parens)

    +

    A variable

    + `.trimReturns()); + }); +}); + +describe('Math', ()=>{ + it('Handles simple math using numbers only', function() { + const source = dedent` + $[1 + 3 * 5 - (1 / 4)] + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    15.75

    '); + }); + + it('Handles round function', function() { + const source = dedent` + $[round(1/4)]`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    0

    '); + }); + + it('Handles floor function', function() { + const source = dedent` + $[floor(0.6)]`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    0

    '); + }); + + it('Handles ceil function', function() { + const source = dedent` + $[ceil(0.2)]`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    1

    '); + }); + + it('Handles nested functions', function() { + const source = dedent` + $[ceil(floor(round(0.6)))]`; + const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    1

    '); + }); + + it('Handles simple math with variables', function() { + const source = dedent` + $[num1]: 5 + + $[num2]: 4 + + Answer is $[answer]($[1 + 3 * num1 - (1 / num2)]). + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    Answer is 15.75.

    '); + }); + + it('Handles variable incrementing', function() { + const source = dedent` + $[num1]: 5 + + Increment num1 to get $[num1]($[num1 + 1]) and again to $[num1]($[num1 + 1]). + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    Increment num1 to get 6 and again to 7.

    '); + }); +}); + +describe('Code blocks', ()=>{ + it('Ignores all variables in fenced code blocks', function() { + const source = dedent` + \`\`\` + [var]: string + + $[var] + + $[var](new string) + \`\`\` + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +
    
    +		 [var]: string
    +		 
    +		 $[var]
    +		 
    +		 $[var](new string)
    +		 
    `.trimReturns()); + }); + + it('Ignores all variables in indented code blocks', function() { + const source = dedent` + test + + [var]: string + + $[var] + + $[var](new string) + `; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    test

    + +
    
    +		 [var]: string
    +		 
    +		 $[var]
    +		 
    +		 $[var](new string)
    +		 
    `.trimReturns()); + }); + + it('Ignores all variables in inline code blocks', function() { + const source = '[var](Hello) `[link](url)`. This `[var] does not work`'; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    var [link](url). This [var] does not work

    `.trimReturns()); + }); +}); + +describe('Normal Links and Images', ()=>{ + it('Renders normal images', function() { + const source = `![alt text](url)`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    alt text

    `.trimReturns()); + }); + + it('Renders normal images with a title', function() { + const source = 'An image ![alt text](url "and title")!'; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    An image alt text!

    `.trimReturns()); + }); + + it('Applies curly injectors to images', function() { + 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()); + }); + + it('Renders normal links', function() { + const source = 'A Link to my [website](url)!'; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    A Link to my website!

    `.trimReturns()); + }); + + it('Renders normal links with a title', function() { + const source = 'A Link to my [website](url "and title")!'; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` +

    A Link to my website!

    `.trimReturns()); + }); +}); + +describe('Cross-page variables', ()=>{ + it('Handles variable assignment and recall across pages', function() { + const source0 = `[var]: string`; + const source1 = `$[var]`; + const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); + expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('\\page

    string

    '); + }); + + it('Handles hoisting across pages', function() { + const source0 = `$[var]`; + const source1 = `[var]: string`; + renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up + const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); + expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('

    string

    \\page'); + }); + + it('Handles reassignment and hoisting across pages', function() { + const source0 = `$[var]\n\n[var]: one\n\n$[var]`; + const source1 = `[var]: two\n\n$[var]`; + renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up + const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); + expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('

    two

    one

    \\page

    two

    '); + }); +}); \ No newline at end of file