0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-09 07:12:40 +00:00

Merge pull request #3290 from naturalcrit/PreprocessVars

Simplify and clean Vars - Done in preprocessing step now
This commit is contained in:
Trevor Buckner
2024-02-09 02:04:02 -05:00
committed by GitHub
9 changed files with 15332 additions and 15279 deletions

View File

@@ -4,6 +4,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){ module.exports = function(props){
return <Nav.item return <Nav.item
href='/new' href='/new'
newTab={true}
color='purple' color='purple'
icon='fas fa-plus-square'> icon='fas fa-plus-square'>
new new

View File

@@ -125,7 +125,7 @@ const BrewItem = createClass({
<div className='info'> <div className='info'>
{brew.tags?.length ? <> {brew.tags?.length ? <>
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}> <div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
<i className='fas fa-tags'/> <i className='fas fa-tags'/>
{brew.tags.map((tag, idx)=>{ {brew.tags.map((tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/); const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
@@ -135,7 +135,7 @@ const BrewItem = createClass({
</> : <></> </> : <></>
} }
<span title={`Authors:\n${brew.authors?.join('\n')}`}> <span title={`Authors:\n${brew.authors?.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors?.join(', ')} <i className='fas fa-user'/> {brew.authors.map((item) => <a href={`/user/${item}`}>{item}</a>)}
</span> </span>
<br /> <br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}> <span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>

View File

@@ -48,6 +48,10 @@
&>span{ &>span{
margin-right : 12px; margin-right : 12px;
line-height : 1.5em; line-height : 1.5em;
a {
color:inherit;
}
} }
} }
.brewTags span { .brewTags span {

30038
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.10.0", "version": "3.10.0",
"engines": { "engines": {
"npm": "^10.2.x", "npm": "^10.2.x",
"node": "^20.8.x" "node": "^20.8.x"
}, },
"repository": { "repository": {
@@ -79,17 +79,18 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.23.7", "@babel/core": "^7.23.9",
"@babel/plugin-transform-runtime": "^7.23.7", "@babel/plugin-transform-runtime": "^7.23.9",
"@babel/preset-env": "^7.23.8", "@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.23.3",
"@googleapis/drive": "^8.6.0", "@googleapis/drive": "^8.7.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"expr-eval": "^2.0.2",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.7",
@@ -98,19 +99,19 @@
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "5.1.1", "marked": "11.2.0",
"marked-extended-tables": "^1.0.8", "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", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.1.0", "mongoose": "^8.1.1",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.21.3", "react-router-dom": "6.22.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^8.1.2", "superagent": "^8.1.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
@@ -123,7 +124,7 @@
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^15.11.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-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3", "stylelint-stylistic": "^0.4.3",
"supertest": "^6.3.4" "supertest": "^6.3.4"

View File

@@ -26,7 +26,6 @@
"codemirror/addon/edit/trailingspace.js", "codemirror/addon/edit/trailingspace.js",
"codemirror/addon/selection/active-line.js", "codemirror/addon/selection/active-line.js",
"moment", "moment",
"superagent", "superagent"
"marked"
] ]
} }

View File

@@ -26,85 +26,124 @@ const mw = {
} }
}; };
const junkBrewPipeline = [
/* Search for brews that are older than 3 days and that are shorter than a tweet */ { $match : {
const junkBrewQuery = HomebrewModel.find({ updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
'$where' : 'this.text.length < 140', lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
createdAt : { }},
$lt : Moment().subtract(30, 'days').toDate() { $project: { textBinSize: { $binarySize: '$textBin' } } },
} { $match: { textBinSize: { $lt: 140 } } },
}).limit(100).maxTime(60000); { $limit: 100 }
];
/* Search for brews that aren't compressed (missing the compressed text field) */ /* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({ const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true } 'text' : { '$exists': true }
}).lean().limit(10000).select('_id'); }).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)=>{ router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
junkBrewQuery.exec((err, objs)=>{ HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
if(err) return res.status(500).send(err); .then((objs)=>res.json({ count: objs.length }))
return 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)=>{ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
junkBrewQuery.remove().exec((err, objs)=>{ HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
if(err) return res.status(500).send(err); .then((docs)=>{
return res.json({ count: objs.length }); 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 */ /* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{ router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
HomebrewModel.findOne({ $or : [ HomebrewModel.findOne({
{ editId: { '$regex': req.params.id, '$options': 'i' } }, $or : [
{ shareId: { '$regex': req.params.id, '$options': 'i' } }, { editId: { $regex: req.params.id, $options: 'i' } },
] }).exec((err, brew)=>{ { shareId: { $regex: req.params.id, $options: 'i' } },
return res.json(brew); ]
}).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 */ /* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
uncompressedBrewQuery.exec((err, objs)=>{ const query = uncompressedBrewQuery.clone();
if(err) return res.status(500).send(err);
objs = objs.map((obj)=>{return obj._id;}); query.exec()
return res.json({ count: objs.length, ids: objs }); .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 */ /* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{ router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.get({ _id: req.params.id }) HomebrewModel.findOne({ _id: req.params.id })
.then((brew)=>{ .then((brew)=>{
brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving if(!brew)
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore return res.status(404).send('Brew not found');
brew.save((err, obj)=>{ if(brew.text) {
if(err) throw err; brew.textBin = brew.textBin || zlib.deflateRawSync(brew.text); //Don't overwrite textBin if exists
return res.status(200).send(obj); brew.text = undefined;
}); }
return brew.save();
}) })
.then((obj)=>res.status(200).send(obj))
.catch((err)=>{ .catch((err)=>{
console.log(err); console.error(err);
return res.status(500).send('Error while saving'); 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({ 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)=>{ router.get('/admin', mw.adminOnly, (req, res)=>{
templateFn('admin', { templateFn('admin', {
url : req.originalUrl url : req.originalUrl
}) })
.then((page)=>res.send(page)) .then((page)=>res.send(page))
.catch((err)=>res.sendStatus(500)); .catch((err)=>res.sendStatus(500));
}); });
module.exports = router; module.exports = router;

View File

@@ -304,7 +304,8 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
text : req.brew.text, text : req.brew.text,
style : req.brew.style, style : req.brew.style,
renderer : req.brew.renderer, renderer : req.brew.renderer,
theme : req.brew.theme theme : req.brew.theme,
tags : req.brew.tags
}; };
req.brew = _.defaults(brew, DEFAULT_BREW); req.brew = _.defaults(brew, DEFAULT_BREW);

View File

@@ -4,9 +4,39 @@ const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables'); const MarkedExtendedTables = require('marked-extended-tables');
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite'); const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id'); const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
const MathParser = require('expr-eval').Parser;
const renderer = new Marked.Renderer(); const renderer = new Marked.Renderer();
const tokenizer = new Marked.Tokenizer(); 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,
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, ceil : false, floor: false, trunc : 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, join : false, sum : 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 //Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) { renderer.html = function (html) {
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){ if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
@@ -29,9 +59,26 @@ renderer.paragraph = function(text){
return `<p>${text}</p>\n`; return `<p>${text}</p>\n`;
}; };
//TODO: may not be needed //Fix local links in the Preview iFrame to link inside the frame
// Disable default reflink definitions renderer.link = function (href, title, text) {
tokenizer.def = function(){ let self = false;
if(href[0] == '#') {
self = true;
}
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
if(href === null) {
return text;
}
let out = `<a href="${escape(href)}"`;
if(title) {
out += ` title="${title}"`;
}
if(self) {
out += ' target="_self"';
}
out += `>${text}</a>`;
return out;
}; };
const mustacheSpans = { const mustacheSpans = {
@@ -273,7 +320,7 @@ const definitionLists = {
}; };
//v=====--------------------< Variable Handling >-------------------=====v// 295 lines //v=====--------------------< Variable Handling >-------------------=====v// 258 lines
const replaceVar = function(input, hoist=false) { const replaceVar = function(input, hoist=false) {
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)/g; const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)/g;
const match = regex.exec(input); const match = regex.exec(input);
@@ -284,36 +331,31 @@ const replaceVar = function(input, hoist=false) {
let missingValues = []; let missingValues = [];
//v=====--------------------< HANDLE MATH >-------------------=====v// //v=====--------------------< HANDLE MATH >-------------------=====v//
const mathRegex = /[^+\-*\/]+|[+\-*\/]/g; const variableRegex = /[a-zA-Z_][a-zA-Z0-9_]*(?=\s*(?:[+\-*\/()]|$))/g; // Capture only variables, ignore mathy stuff
let mathLabels = label.match(mathRegex).map((s)=>s.trim()); let mathVars = label.match(variableRegex)?.map((s)=>s.trim());
if(mathLabels.length > 2 && mathLabels.length % 2 == 1) { let replacedLabel = label;
const valid = mathLabels.every((val, i)=>{ // Math must alternate between operators and values if(mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math!
const isOperator = '+-*/'.includes(val); mathVars?.forEach((variable) => {
return (i % 2 === 0 ? !isOperator : isOperator); 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
if(!valid) replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
return { value: input, missingValues: missingValues }; }
else {
mathLabels = mathLabels.map((str)=>{ missingValues.push(foundVar);
if(!isNaN(str)) }
return Number(str);
if('+-*/'.includes(str))
return str;
const foundVar = lookupVar(str, globalPageNumber, hoist);
if(foundVar && foundVar.resolved && foundVar.content) // Only subsitute math values if fully resolved and not empty strings
return foundVar.content;
return str;
}); });
missingValues = mathLabels.filter((x)=>isNaN(x) && !'+-*/'.includes(x)); let result;
try {
result = mathParser.evaluate(replacedLabel);
} catch (error) {
result = input;
}
return { return {
value : missingValues.length > 0 ? input : eval(mathLabels.join('')), value : result,
missingValues : missingValues missingValues : missingValues
}; };
} }
@@ -345,10 +387,10 @@ const replaceVar = function(input, hoist=false) {
let value; let value;
if(!prefix[0] && href) // Link if(!prefix[0] && href) // Link
value = `[${label}](${href} ${title ? title : ''})`; value = `[${label}](${href}${title ? ` ${title}` : ''})`;
if(prefix[0] == '!' && href) // Image if(prefix[0] == '!' && href) // Image
value = `![${label}](${href} ${title ? title : ''})`; value = `![${label}](${href} ${title ? ` ${title}` : ''})`;
if(prefix[0] == '$') // Variable if(prefix[0] == '$') // Variable
value = foundVar.content; value = foundVar.content;
@@ -376,209 +418,169 @@ const lookupVar = function(label, index, hoist=false) {
const processVariableQueue = function() { const processVariableQueue = function() {
let resolvedOne = true; let resolvedOne = true;
let finalLoop = false; let finalLoop = false;
let newQueue = [];
while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved
newQueue = [];
resolvedOne = false; resolvedOne = false;
for (const item of linksQueue) { for (const item of linksQueue) {
const value = replaceVar(item.match, true); if(item.type == 'text')
continue;
if(value.missingValues.length > 0 && !finalLoop) { // Variable not found; try again next loop. if(item.type == 'varDefBlock' || item.type == 'varDefInline') {
newQueue.push(item); // If previous loops could not resolve any new vars, const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
continue; // final loop will just use the best value so far let match;
} // (may be only partially resolved) let resolved = true;
let tempContent = item.content;
while (match = regex.exec(item.content)) { // regex to find variable calls
const value = replaceVar(match[0], true);
resolvedOne = true; if(value.missingValues.length > 0) {
resolved = false;
} else {
item.content = item.content.replaceAll(match[0], value.value);
}
}
item.token.content = item.token.content.replace(item.match, value.value); if(resolved == true || item.content != tempContent) {
resolvedOne = true;
}
if(item.token.type == 'varDefBlock' || item.token.type == 'varDefInline') { globalLinks[globalPageNumber][item.varName] = {
globalLinks[globalPageNumber][item.token.label] = { content : item.content,
content : item.token.content, resolved : resolved
resolved : true
}; };
if(item.type == 'varDefBlock' && resolved){
item.type = 'resolved';
}
if(item.type == 'varDefInline' && resolved){
item.type = 'text';
}
}
if(item.type == 'varCallBlock' || item.type == 'varCallInline') {
const value = replaceVar(item.match, true);
if(value.missingValues.length > 0 && !finalLoop) { // Variable not found or not fully resolved; try again next loop.
continue; // final loop will just use the best value so far
}
resolvedOne = true;
item.content = item.content.replace(item.match, value.value);
item.type = 'text';
} }
} }
linksQueue = newQueue; linksQueue = linksQueue.filter(item => item.type !== 'resolved'); // Remove any fully-resolved variables
if(finalLoop) if(finalLoop)
break; break;
if(!resolvedOne) if(!resolvedOne)
finalLoop = true; finalLoop = true;
} }
linksQueue = linksQueue.filter(item => item.type !== 'varDefBlock');
}; };
const walkVariableTokens = { function MarkedVariables() {
walkTokens(token) { return {
if(token.type == 'varDefBlock' || token.type == 'varDefInline') { hooks: {
preprocess(src) {
const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 1, [2]:3
const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 4, [5]
const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+?)\)/; //Matches 6, 7[8](9)
const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 10, [11]
const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Combine regexes like so: (regex1)|(regex2)|(regex3)|(regex4)
let match; let combinedRegex = new RegExp([blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map(s => `(${s.source})`).join('|'), 'gm');
let resolved = true;
while (match = regex.exec(token.content)) { // regex to find variable calls
const value = replaceVar(match[0]);
if(value.missingValues.length > 0) { let lastIndex = 0;
for (let i = 0; i < value.missingValues.length; i++) { let match;
linksQueue.push({ token: token, match: match[0], varName: value.missingValues[i] }); while ((match = combinedRegex.exec(src)) !== null) {
} // Form any matches into tokens and store
resolved = false; if (match.index > lastIndex) {
} else { linksQueue.push(
token.content = token.content.replace(match[0], value.value); { type : 'text',
match : src.slice(lastIndex, match.index),
varName : null,
content : src.slice(lastIndex, match.index)
});
}
if(match[1]) { // Block Definition
const label = match[2] ? match[2].trim().replace(/\s+/g, ' ').toLowerCase() : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
const content = match[3] ? match[3].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
linksQueue.push(
{ type : 'varDefBlock',
match : match[0],
varName : label,
content : content
});
}
if(match[4]) { // Block Call
const label = match[5] ? match[5].trim().replace(/\s+/g, ' ').toLowerCase() : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
linksQueue.push(
{ type : 'varCallBlock',
match : match[0],
varName : label,
content : match[0]
});
}
if(match[6]) { // Inline Definition
const label = match[8] ? match[8].trim().replace(/\s+/g, ' ').toLowerCase() : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
const content = match[9] ? match[9].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
linksQueue.push(
{ type : 'varDefBlock',
match : match[0],
varName : label,
content : content
});
linksQueue.push(
{ type : 'varCallInline',
match : match[7],
varName : label,
content : match[7]
});
}
if(match[10]) { // Inline Call
const label = match[11] ? match[11].trim().replace(/\s+/g, ' ').toLowerCase() : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
linksQueue.push(
{ type : 'varCallInline',
match : match[0],
varName : label,
content : match[0]
});
}
lastIndex = combinedRegex.lastIndex;
} }
}
globalLinks[globalPageNumber][token.label] = { if (lastIndex < src.length) {
content : token.content, linksQueue.push(
resolved : resolved { type : 'text',
}; match : src.slice(lastIndex),
if(token.type == 'varDefInline') //Inline definitions are also inline calls; after storing the value, change type so it can be displayed varName : null,
token.type = 'varCallInline'; content : src.slice(lastIndex)
} });
if(token.type == 'varCallBlock' || token.type == 'varCallInline' || token.originalType == 'varCallBlock' || token.originalType == 'varCallInline') {
const value = replaceVar(token.raw);
if(value.missingValues.length > 0) {
for (let i = 0; i < value.missingValues.length; i++) {
linksQueue.push({ token: token, match: token.raw, varName: value.missingValues[i] });
} }
return;
}
token.content = token.content.replace(token.content, value.value); processVariableQueue();
}
}
};
const varDefBlock = { const output = linksQueue.map(item => item.content).join('');
name : 'varDefBlock', linksQueue = []; // Must clear linksQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array
level : 'block', return output;
start(src) {return src.match(/\n {0,3}[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]:/m)?.index; }, }
tokenizer(src, tokens) { }
// [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines) };
const regex = /^ {0,3}[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/;
const match = regex.exec(src);
if(match) {
const label = match[1] ? match[1].trim().replace(/\s+/g, ' ').toLowerCase() : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
const content = match[2] ? match[2].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
return {
type : 'varDefBlock',
raw : match[0],
label : label,
content : content
};
}
},
renderer(token){
return;
}
};
const varDefInline = {
name : 'varDefInline',
level : 'inline',
start(src) {return src.match(/\n?[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]:\(.*\)/m)?.index;},
tokenizer(src, tokens) {
if(!parseVars) //Don't re-parse variable defs inside of another variable call
return;
// [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines)
const regex = /^\n?([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:\((.+?)\)/;
const match = regex.exec(src);
if(match) {
const label = match[2] ? match[2].trim().replace(/\s+/g, ' ').toLowerCase() : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
const content = match[3] ? match[3].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
return {
type : 'varDefInline',
raw : match[0],
label : label,
content : content
};
}
},
renderer(token) {
return;
}
};
const varCallBlock = {
name : 'varCallBlock',
level : 'block',
start(src) {return src.match(/\n[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]/m)?.index;},
tokenizer(src, tokens) {
if(!parseVars) //Don't re-parse variable calls inside of another variable call
return;
// [ variable name (spaces allowed) ] no following text allowed
const regex = /^([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/;
const match = regex.exec(src);
if(match) {
return {
type : 'varCallBlock',
raw : match[0],
content : match[0]
};
}
},
renderer(token){
const tokens = new Marked.Lexer(Marked.defaults).lex(token.content);
return this.parser.parse(tokens);
}
};
const varCallInline = {
name : 'varCallInline',
level : 'inline',
start(src) {return src.match(/[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]/m)?.index;},
tokenizer(src, tokens) {
if(!parseVars) //Don't re-parse variable calls inside of another variable call
return;
// [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines)
const regex = /^([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; // Do not allow `(` after, since that is needed for normal images/links
const match = regex.exec(src);
if(match) {
return {
type : 'varCallInline',
raw : match[0],
content : match[0]
};
}
},
renderer(token){
const tokens = new Marked.Lexer(Marked.defaults).inlineTokens(token.content);
return this.parser.parseInline(tokens);
}
}; };
//^=====--------------------< Variable Handling >-------------------=====^// //^=====--------------------< Variable Handling >-------------------=====^//
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts, varCallInline, varDefInline, varCallBlock, varDefBlock] }); Marked.use(MarkedVariables())
Marked.use(mustacheInjectBlock, walkVariableTokens); Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite()); 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 = `<a href="${escape(href)}"`;
if(title) {
out += ` title="${title}"`;
}
if(self) {
out += ' target="_self"';
}
out += `>${text}</a>`;
return out;
};
const nonWordAndColonTest = /[^\w:]/g; const nonWordAndColonTest = /[^\w:]/g;
const cleanUrl = function (sanitize, base, href) { const cleanUrl = function (sanitize, base, href) {
if(sanitize) { if(sanitize) {
@@ -664,7 +666,7 @@ module.exports = {
marked : Marked, marked : Marked,
render : (rawBrewText, pageNumber=1)=>{ render : (rawBrewText, pageNumber=1)=>{
globalLinks[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order globalLinks[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
linksQueue = []; linksQueue = []; //Could move into MarkedVariables()
globalPageNumber = pageNumber; globalPageNumber = pageNumber;
parseVars = true; parseVars = true;
@@ -677,9 +679,9 @@ module.exports = {
const tokens = Marked.lexer(rawBrewText, opts); const tokens = Marked.lexer(rawBrewText, opts);
Marked.walkTokens(tokens, opts.walkTokens); Marked.walkTokens(tokens, opts.walkTokens);
processVariableQueue();
parseVars = false; parseVars = false;
const html = Marked.parser(tokens, opts); const html = Marked.parser(tokens, opts);
return opts.hooks.postprocess(html); return opts.hooks.postprocess(html);
}, },