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 `` 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()}
- ;
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 ``;
+
+ 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
+ H1
+ H2
+ A
+ B
+ C
+ D
+
`.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 = ``;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
+
`.trimReturns());
+ });
+
+ it('Renders normal images with a title', function() {
+ const source = 'An image !';
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
+ An image !
`.trimReturns());
+ });
+
+ it('Applies curly injectors to images', function() {
+ const source = `{width:100px}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
+
`.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('\\pagestring
');
+ });
+
+ 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
\\pagetwo
');
+ });
+});
\ No newline at end of file