From 3b89bba93c4a8df4279c6da0123e94311a6b1756 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Tue, 20 Jan 2026 00:24:40 -0600 Subject: [PATCH 01/52] Template class persistence. --- client/homebrew/brewRenderer/brewRenderer.jsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 7e6681c57..e1934985c 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -88,6 +88,7 @@ const BrewPage = (props)=>{ //v=====--------------------< Brew Renderer Component >-------------------=====v// let renderedPages = []; +let pageTemplates = []; let rawPages = []; const BrewRenderer = (props)=>{ @@ -200,11 +201,25 @@ const BrewRenderer = (props)=>{ const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens; const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags; if(injectedTags) { + const processedClassNames = injectedTags.classes.split(' ').map((className)=>{ + if(className.startsWith('@')) { // Look for a "classname" with a template prefix "@" + pageTemplates[index] = className.slice(1); // Store it in the lookup array without the prefix + return className.slice(1); // return the value without the prefix. + } + return className; + }).join(' '); styles = { ...styles, ...injectedTags.styles }; styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React - classes = [classes, injectedTags.classes].join(' ').trim(); + classes = [classes, processedClassNames].join(' ').trim(); attributes = injectedTags.attributes; } + // If we don't have a template for this page, look backwards until one is found or the first page. + if(!pageTemplates[index]) { + for(let i=index;i>=0; i--) { + // If one is found, insert the template class name after 'page' + if(pageTemplates[i]) classes = classes.replace('page', `page ${pageTemplates[i]}`); + } + } pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line } @@ -221,8 +236,10 @@ const BrewRenderer = (props)=>{ if(props.errors && props.errors.length) return renderedPages; - if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes + if(rawPages.length != renderedPages.length) { // Re-render all pages when page count changes renderedPages.length = 0; + pageTemplates.length = 0; + } // Render currently-edited page first so cross-page effects (variables, links) can propagate out first if(rawPages.length > props.currentEditorCursorPageNum -1) From 153fa3abb56f9adee958773ed9e27b94b9928d91 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Tue, 20 Jan 2026 00:34:41 -0600 Subject: [PATCH 02/52] Replace page instead of inserting after it. --- client/homebrew/brewRenderer/brewRenderer.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index e1934985c..ded125fd1 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -204,6 +204,7 @@ const BrewRenderer = (props)=>{ const processedClassNames = injectedTags.classes.split(' ').map((className)=>{ if(className.startsWith('@')) { // Look for a "classname" with a template prefix "@" pageTemplates[index] = className.slice(1); // Store it in the lookup array without the prefix + classes = ''; // Strip the default Template class from assignment. return className.slice(1); // return the value without the prefix. } return className; @@ -215,9 +216,9 @@ const BrewRenderer = (props)=>{ } // If we don't have a template for this page, look backwards until one is found or the first page. if(!pageTemplates[index]) { - for(let i=index;i>=0; i--) { + for (let i=index;i>=0; i--) { // If one is found, insert the template class name after 'page' - if(pageTemplates[i]) classes = classes.replace('page', `page ${pageTemplates[i]}`); + if (pageTemplates[i]) classes = classes.replace( /^page/, `${pageTemplates[i]}`); } } pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line From 3c7d7339f0926719ca24ce7de9864c308f84c835 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Tue, 20 Jan 2026 22:33:41 -0600 Subject: [PATCH 03/52] Shift Template designation to much simpler and cleaner attribute --- client/homebrew/brewRenderer/brewRenderer.jsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index ded125fd1..461d2fea6 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -201,24 +201,19 @@ const BrewRenderer = (props)=>{ const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens; const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags; if(injectedTags) { - const processedClassNames = injectedTags.classes.split(' ').map((className)=>{ - if(className.startsWith('@')) { // Look for a "classname" with a template prefix "@" - pageTemplates[index] = className.slice(1); // Store it in the lookup array without the prefix - classes = ''; // Strip the default Template class from assignment. - return className.slice(1); // return the value without the prefix. - } - return className; - }).join(' '); styles = { ...styles, ...injectedTags.styles }; styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React - classes = [classes, processedClassNames].join(' ').trim(); + classes = [classes, injectedTags.classes].join(' ').trim(); attributes = injectedTags.attributes; + if (attributes && Object.hasOwn(attributes, 'hbtemplate')) { + pageTemplates[index] = attributes['hbtemplate']; + } } // If we don't have a template for this page, look backwards until one is found or the first page. if(!pageTemplates[index]) { for (let i=index;i>=0; i--) { - // If one is found, insert the template class name after 'page' - if (pageTemplates[i]) classes = classes.replace( /^page/, `${pageTemplates[i]}`); + // If one is found, add the template attribute + if (pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i]; } } pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line From 33edc9600ab832c82a1b5b3a0cec904e02cab560 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Fri, 27 Feb 2026 16:51:46 -0600 Subject: [PATCH 04/52] Enable use of enable_v4 config variable --- client/homebrew/brewRenderer/brewRenderer.jsx | 18 +++++++++------ client/homebrew/homebrew.jsx | 4 +++- config/default.json | 3 ++- package-lock.json | 22 +------------------ server/app.js | 3 ++- 5 files changed, 19 insertions(+), 31 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 2307ee3b1..a1eebab31 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -204,15 +204,19 @@ const BrewRenderer = (props)=>{ styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React classes = [classes, injectedTags.classes].join(' ').trim(); attributes = injectedTags.attributes; - if (attributes && Object.hasOwn(attributes, 'hbtemplate')) { - pageTemplates[index] = attributes['hbtemplate']; + if(global.enable_v4) { + if (attributes && Object.hasOwn(attributes, 'hbtemplate')) { + pageTemplates[index] = attributes['hbtemplate']; + } } } - // If we don't have a template for this page, look backwards until one is found or the first page. - if(!pageTemplates[index]) { - for (let i=index;i>=0; i--) { - // If one is found, add the template attribute - if (pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i]; + if(global.enable_v4) { + // If we don't have a template for this page, look backwards until one is found or the first page. + if(!pageTemplates[index]) { + for (let i=index;i>=0; i--) { + // If one is found, add the template attribute + if (pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i]; + } } } pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 326287ee6..a3a16bef3 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -38,12 +38,14 @@ const Homebrew = (props)=>{ lang : '' }, userThemes, - brews + brews, + enable_v4 } = props; global.account = account; global.version = version; global.config = config; + global.enable_v4 = enable_v4; const backgroundObject = ()=>{ if(global.config.deployment || (config.local && config.development)){ diff --git a/config/default.json b/config/default.json index 6be4ce7ce..e1aca3940 100644 --- a/config/default.json +++ b/config/default.json @@ -7,5 +7,6 @@ "local_environments" : ["docker", "local"], "publicUrl" : "https://homebrewery.naturalcrit.com", "hb_images" : null, - "hb_fonts" : null + "hb_fonts" : null, + "enable_v4" : true } diff --git a/package-lock.json b/package-lock.json index 2b3892c88..aa225f518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,7 +175,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -199,7 +198,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -273,7 +271,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1951,7 +1948,6 @@ "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -2077,7 +2073,6 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2139,7 +2134,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2180,7 +2174,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3011,7 +3004,6 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3781,7 +3773,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4788,7 +4779,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6353,7 +6343,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8958,7 +8947,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9590,7 +9578,6 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -10084,7 +10071,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -11691,7 +11677,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11754,7 +11739,6 @@ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11769,6 +11753,7 @@ "integrity": "sha512-Mn8KJ45HNNG6JBpBizXcyf6LqY/qyqetGcou/nprDnFwBFBLGj0j/sNKV2lj2KMOVOwdXu14aEzqJv8CIV6e8g==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "postcss": "^8.4.20" } @@ -11857,7 +11842,6 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -12047,7 +12031,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12060,7 +12043,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13663,7 +13645,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-syntax-patches-for-csstree": "^1.0.19", @@ -14185,7 +14166,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/server/app.js b/server/app.js index 1bdb5aac3..25ac8e909 100644 --- a/server/app.js +++ b/server/app.js @@ -565,7 +565,8 @@ const renderPage = async (req, res)=>{ account : req.account, config : configuration, ogMeta : req.ogMeta, - userThemes : req.userThemes + userThemes : req.userThemes, + enable_v4 : config.get('enable_v4') }; const title = req.brew ? req.brew.title : ''; const page = await templateFn('homebrew', title, props) From 2a997404e37ce2bc7d2902a07e605005267bacf8 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Sun, 19 Apr 2026 14:24:27 -0500 Subject: [PATCH 05/52] A plausible fix for the hoisting issue I don't know if it's the *best* fix. --- client/homebrew/brewRenderer/brewRenderer.jsx | 7 +- package-lock.json | 85 ++++++++----------- package.json | 2 +- 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index efdfce5c9..753c9a2c9 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -220,7 +220,8 @@ const BrewRenderer = (props)=>{ } }; - const renderPages = ()=>{ + const renderPages = (forceRender = false)=>{ + if(props.errors && props.errors.length) return renderedPages; @@ -232,7 +233,7 @@ const BrewRenderer = (props)=>{ renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ - if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ + if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){ renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range } }); @@ -280,7 +281,7 @@ const BrewRenderer = (props)=>{ }); setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame - renderPages(); //Make sure page is renderable before showing + renderPages(true); //Make sure page is renderable before showing setState((prevState)=>({ ...prevState, isMounted : true, diff --git a/package-lock.json b/package-lock.json index b67bcb18f..b6910697e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.4", - "marked-variables": "^1.0.5", + "marked-variables": "file:../marked-variables", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", "mongoose": "^9.3.3", @@ -85,6 +85,38 @@ "npm": ">=10.8 <12" } }, + "../marked-variables": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "expr-eval": "^2.0.2", + "romans": "^3.1.0", + "written-number": "^0.11.1" + }, + "devDependencies": { + "@babel/core": "^7.28.6", + "@babel/preset-env": "^7.26.0", + "@markedjs/testutils": "17.0.1-2", + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^12.0.2", + "@semantic-release/npm": "^13.1.3", + "@semantic-release/release-notes-generator": "^14.0.2", + "babel-jest": "^30.2.0", + "dedent-tabs": "^0.10.3", + "eslint": "^8.57.1", + "jest": "^30.2.0", + "jest-cli": "^30.2.0", + "rollup": "^4.29.2", + "semantic-release": "^25.0.2" + }, + "peerDependencies": { + "marked": ">=3 <18" + } + }, "node_modules/@acemir/cssom": { "version": "0.9.31", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", @@ -178,7 +210,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2011,7 +2042,6 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2125,7 +2155,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -2174,7 +2203,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -3394,7 +3422,6 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -4530,7 +4557,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5116,7 +5142,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6290,7 +6315,6 @@ "integrity": "sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -6648,12 +6672,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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==", - "license": "MIT" - }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -8553,7 +8571,6 @@ "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.3.0", "@jest/types": "30.3.0", @@ -9171,7 +9188,6 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -9549,7 +9565,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -9633,18 +9648,8 @@ } }, "node_modules/marked-variables": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-1.0.5.tgz", - "integrity": "sha512-iDsQoXA4lD1ZdPk0Rh6lHRZ2pPNRpd+dfojKvqlbgHw8VhlJfd9YBMK4Zf3McNpkF33VllJ2WbhRCfWw56gOfw==", - "license": "MIT", - "dependencies": { - "expr-eval": "^2.0.2", - "romans": "^3.1.0", - "written-number": "^0.11.1" - }, - "peerDependencies": { - "marked": ">=3 <18" - } + "resolved": "../marked-variables", + "link": true }, "node_modules/markedLegacy": { "name": "marked", @@ -10752,7 +10757,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10808,7 +10812,6 @@ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10823,6 +10826,7 @@ "integrity": "sha512-TXbU+h6vVRW+86c/+ewhWq9k7pr7ijASTnepVhCQiC87zAOTkvB1v2dHyWP+ggstSTX/PNvjzS+IOqzejndz9w==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "postcss": "^8.4.20" } @@ -10895,7 +10899,6 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -11039,7 +11042,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11049,7 +11051,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11333,12 +11334,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/romans": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/romans/-/romans-3.1.0.tgz", - "integrity": "sha512-URNEK8THW0suBPXUhwtHIK0CkKrtochZ8LhMPTbFoMKLtx46ImFVU+k/OaR2hdcWU/IiUeDpsKfAMKN45+79EQ==", - "license": "MIT" - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -12117,7 +12112,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", @@ -13060,7 +13054,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13433,12 +13426,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/written-number": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/written-number/-/written-number-0.11.1.tgz", - "integrity": "sha512-LhQ68uUnzHH0bwm/QiGA9JwqgadSDOwqB2AIs/LBsrOY6ScqVXKRN2slTCeKAhstDBJ/Of/Yxcjn0pnQmVlmtg==", - "license": "MIT" - }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index 2ac02e4a1..e243a7a30 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.4", - "marked-variables": "^1.0.5", + "marked-variables": "file:../marked-variables", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", "mongoose": "^9.3.3", From 59cdfcb19e042828e8664c33067d1003e82e6d3b Mon Sep 17 00:00:00 2001 From: David Bolack Date: Sun, 19 Apr 2026 14:34:30 -0500 Subject: [PATCH 06/52] Rework "force" to attempt to find variable references and only force those pages --- client/homebrew/brewRenderer/brewRenderer.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 753c9a2c9..851495d64 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -220,7 +220,7 @@ const BrewRenderer = (props)=>{ } }; - const renderPages = (forceRender = false)=>{ + const renderPages = (checkHoists = false)=>{ if(props.errors && props.errors.length) return renderedPages; @@ -233,6 +233,7 @@ const BrewRenderer = (props)=>{ renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ + const forceRender = checkHoists && (page.match(/([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g)); if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){ renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range } From 55acb1dc607e64e0c7cc09cc0e6e2f2074eb8e93 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Mon, 20 Apr 2026 14:00:42 -0500 Subject: [PATCH 07/52] CORRECTLY revert from testing against local marked-variables. --- package-lock.json | 66 ++++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6910697e..c9ba9b3d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.4", - "marked-variables": "file:../marked-variables", + "marked-variables": "^1.0.5", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", "mongoose": "^9.3.3", @@ -85,38 +85,6 @@ "npm": ">=10.8 <12" } }, - "../marked-variables": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "expr-eval": "^2.0.2", - "romans": "^3.1.0", - "written-number": "^0.11.1" - }, - "devDependencies": { - "@babel/core": "^7.28.6", - "@babel/preset-env": "^7.26.0", - "@markedjs/testutils": "17.0.1-2", - "@rollup/plugin-commonjs": "^29.0.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/commit-analyzer": "^13.0.0", - "@semantic-release/git": "^10.0.1", - "@semantic-release/github": "^12.0.2", - "@semantic-release/npm": "^13.1.3", - "@semantic-release/release-notes-generator": "^14.0.2", - "babel-jest": "^30.2.0", - "dedent-tabs": "^0.10.3", - "eslint": "^8.57.1", - "jest": "^30.2.0", - "jest-cli": "^30.2.0", - "rollup": "^4.29.2", - "semantic-release": "^25.0.2" - }, - "peerDependencies": { - "marked": ">=3 <18" - } - }, "node_modules/@acemir/cssom": { "version": "0.9.31", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", @@ -6672,6 +6640,12 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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==", + "license": "MIT" + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -9648,8 +9622,18 @@ } }, "node_modules/marked-variables": { - "resolved": "../marked-variables", - "link": true + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-1.0.5.tgz", + "integrity": "sha512-iDsQoXA4lD1ZdPk0Rh6lHRZ2pPNRpd+dfojKvqlbgHw8VhlJfd9YBMK4Zf3McNpkF33VllJ2WbhRCfWw56gOfw==", + "license": "MIT", + "dependencies": { + "expr-eval": "^2.0.2", + "romans": "^3.1.0", + "written-number": "^0.11.1" + }, + "peerDependencies": { + "marked": ">=3 <18" + } }, "node_modules/markedLegacy": { "name": "marked", @@ -11334,6 +11318,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/romans": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/romans/-/romans-3.1.0.tgz", + "integrity": "sha512-URNEK8THW0suBPXUhwtHIK0CkKrtochZ8LhMPTbFoMKLtx46ImFVU+k/OaR2hdcWU/IiUeDpsKfAMKN45+79EQ==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -13426,6 +13416,12 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/written-number": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/written-number/-/written-number-0.11.1.tgz", + "integrity": "sha512-LhQ68uUnzHH0bwm/QiGA9JwqgadSDOwqB2AIs/LBsrOY6ScqVXKRN2slTCeKAhstDBJ/Of/Yxcjn0pnQmVlmtg==", + "license": "MIT" + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index e243a7a30..2ac02e4a1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.4", - "marked-variables": "file:../marked-variables", + "marked-variables": "^1.0.5", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", "mongoose": "^9.3.3", From cb4bc16c69784d2b5c9386d9035cd4b708f86ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 10:17:28 +0200 Subject: [PATCH 08/52] move codemirror extensions into their own folder --- client/components/codeEditor/codeEditor.jsx | 10 +++++----- .../codeEditor/{ => extensions}/autocompleteEmoji.js | 0 .../codeEditor/{ => extensions}/customFolding.js | 0 .../codeEditor/{ => extensions}/customHighlight.js | 0 .../codeEditor/{ => extensions}/customKeyMaps.js | 0 .../{ => extensions}/legacyCustomHighlight.js | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename client/components/codeEditor/{ => extensions}/autocompleteEmoji.js (100%) rename client/components/codeEditor/{ => extensions}/customFolding.js (100%) rename client/components/codeEditor/{ => extensions}/customHighlight.js (100%) rename client/components/codeEditor/{ => extensions}/customKeyMaps.js (100%) rename client/components/codeEditor/{ => extensions}/legacyCustomHighlight.js (100%) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 5f46fc3e4..3495570ff 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -24,7 +24,7 @@ import { languages } from '@codemirror/language-data'; import { css } from '@codemirror/lang-css'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { html } from '@codemirror/lang-html'; -import { autocompleteEmoji } from './autocompleteEmoji.js'; +import { autocompleteEmoji } from './extensions/autocompleteEmoji.js'; import { searchKeymap, search } from '@codemirror/search'; import { closeBrackets } from '@codemirror/autocomplete'; @@ -40,10 +40,10 @@ const highlightCompartment = new Compartment(); console.log(themes); -import { generalKeymap, markdownKeymap } from './customKeyMaps.js'; -import foldOnPages from './customFolding.js'; -import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js'; -import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js'; +import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; +import foldOnPages from './extensions/customFolding.js'; +import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; +import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; diff --git a/client/components/codeEditor/autocompleteEmoji.js b/client/components/codeEditor/extensions/autocompleteEmoji.js similarity index 100% rename from client/components/codeEditor/autocompleteEmoji.js rename to client/components/codeEditor/extensions/autocompleteEmoji.js diff --git a/client/components/codeEditor/customFolding.js b/client/components/codeEditor/extensions/customFolding.js similarity index 100% rename from client/components/codeEditor/customFolding.js rename to client/components/codeEditor/extensions/customFolding.js diff --git a/client/components/codeEditor/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js similarity index 100% rename from client/components/codeEditor/customHighlight.js rename to client/components/codeEditor/extensions/customHighlight.js diff --git a/client/components/codeEditor/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js similarity index 100% rename from client/components/codeEditor/customKeyMaps.js rename to client/components/codeEditor/extensions/customKeyMaps.js diff --git a/client/components/codeEditor/legacyCustomHighlight.js b/client/components/codeEditor/extensions/legacyCustomHighlight.js similarity index 100% rename from client/components/codeEditor/legacyCustomHighlight.js rename to client/components/codeEditor/extensions/legacyCustomHighlight.js From 0587266e22c07645a8066b4a00467d8700472170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 12:34:16 +0200 Subject: [PATCH 09/52] repair injection highlight and add image previews --- client/components/codeEditor/codeEditor.jsx | 102 ++++++++++++++++-- client/components/codeEditor/codeEditor.less | 38 +++++++ .../codeEditor/extensions/customHighlight.js | 8 +- 3 files changed, 134 insertions(+), 14 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 3495570ff..d677dc65e 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -38,15 +38,30 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themeCompartment = new Compartment(); const highlightCompartment = new Compartment(); -console.log(themes); - import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; +import { syntaxTree } from '@codemirror/language'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; +function getUrl(node, doc) { + let url = null; + + const cursor = node.node.cursor(); + + if (cursor.firstChild()) { + do { + if (cursor.name === "URL") { + url = doc.sliceString(cursor.from, cursor.to); + break; + } + } while (cursor.nextSibling()); + } + + return url; +} const createHighlightPlugin = (renderer, tab)=>{ //this function takes the custom tokens created in the tokenize function in customhighlight files //takes the tokens defined by that function and assigns classes to them @@ -76,21 +91,84 @@ const createHighlightPlugin = (renderer, tab)=>{ let pageCount = 1; let snippetCount = 0; + const tree = syntaxTree(view.state); +tree.iterate({ + enter: (node) => { + if (node.name === "Image") { + const url = getUrl(node, view.state.doc); + + if (!url) return; + + decos.push( + Decoration.mark({ + class: "cm-image", + attributes: { + "style": `--preview-img:url(${url});` + } + }).range(node.from, node.to) + ); + } + } +}); + + tokens.forEach((tok)=>{ const line = view.state.doc.line(tok.line + 1); + // INLINE TOKENS (marks) if(tok.from != null && tok.to != null && tok.from < tok.to) { - decos.push(Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to)); - } else { - decos.push(Decoration.line({ class: `cm-${tok.type}` }).range(line.from)); - if(tok.type === 'pageLine' && tab === 'brewText') { - pageCount++; - line.from === 0 && pageCount--; - decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from)); + const from = line.from + tok.from; + const to = line.from + tok.to; + + const attrs = {}; + console.log(tok); + // attach URL only for links + if(tok.type === 'Image' && tok.url) { + + attrs['data-url'] = tok.url; } + + decos.push( + Decoration.mark({ + class : `cm-${tok.type}`, + ...(Object.keys(attrs).length + ? { attributes: attrs } + : {}) + }).range(from, to) + ); + } + + // LINE TOKENS + else { + decos.push( + Decoration.line({ + class : `cm-${tok.type}` + }).range(line.from) + ); + + if(tok.type === 'pageLine' && tab === 'brewText') { + pageCount++; + if(line.from === 0) pageCount--; + + decos.push( + Decoration.line({ + attributes : { + 'data-page-number' : pageCount + } + }).range(line.from) + ); + } + if(tok.type === 'snippetLine' && tab === 'brewSnippets') { snippetCount++; - decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); + + decos.push( + Decoration.line({ + attributes : { + 'data-page-number' : snippetCount + } + }).range(line.from) + ); } } }); @@ -99,7 +177,9 @@ const createHighlightPlugin = (renderer, tab)=>{ return Decoration.set(decos); } }, - { decorations: (v)=>v.decorations } + { + decorations : (v)=>v.decorations + } ); }; diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 84ed8058d..3594742fd 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -157,6 +157,44 @@ outline : 1px inset #00000055 !important; } + .cm-image[style] { + position: relative; + + &::before, &::after { + content:""; + width:100px; + height:100px; + position:absolute; + bottom:0; + left:0; + translate:0 100%; + display:block; + z-index:1000; + pointer-events: none; + opacity:0; + transition:0.2s opacity; + } + + &::before{ + background-color: #fff; + border-radius:10px; + border:3px solid grey; + } + + &::after { + background-image: var(--preview-img); + background-size:80%; + background-repeat:no-repeat; + background-position:center; + filter:drop-shadow(0 0 5px #0008); + } + + &:hover::before, + &:hover::after { + opacity:1; + } + } + /* Tab character visualization (optional) */ //.cm-tab { // background: url(...) no-repeat right; diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 8797704cc..92761a78b 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -182,13 +182,15 @@ export function tokenizeCustomMarkdown(text) { } if(lineText.includes('{') && lineText.includes('}')) { - const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm; + const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd; let match; + while ((match = injectionRegex.exec(lineText)) !== null) { + console.log(match.indices[1][0]); tokens.push({ line : lineNumber, - from : match.index, - to : match.index + match[1].length, + from : match.indices[1][0], + to : match.indices[1][0] + match[1].length, type : customTags.injection, }); } From fac3ef15fae817928f517a9dcec595439851d40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 12:42:14 +0200 Subject: [PATCH 10/52] format --- client/components/codeEditor/codeEditor.jsx | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index d677dc65e..dbe042d73 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -51,9 +51,9 @@ function getUrl(node, doc) { const cursor = node.node.cursor(); - if (cursor.firstChild()) { + if(cursor.firstChild()) { do { - if (cursor.name === "URL") { + if(cursor.name === 'URL') { url = doc.sliceString(cursor.from, cursor.to); break; } @@ -92,24 +92,24 @@ const createHighlightPlugin = (renderer, tab)=>{ let snippetCount = 0; const tree = syntaxTree(view.state); -tree.iterate({ - enter: (node) => { - if (node.name === "Image") { - const url = getUrl(node, view.state.doc); + tree.iterate({ + enter : (node)=>{ + if(node.name === 'Image') { + const url = getUrl(node, view.state.doc); - if (!url) return; + if(!url) return; - decos.push( - Decoration.mark({ - class: "cm-image", - attributes: { - "style": `--preview-img:url(${url});` + decos.push( + Decoration.mark({ + class : 'cm-image', + attributes : { + 'style' : `--preview-img:url(${url});` + } + }).range(node.from, node.to) + ); + } } - }).range(node.from, node.to) - ); - } - } -}); + }); tokens.forEach((tok)=>{ From 8872adfd95ed1767dc369f35f7b8aa831f9835ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 13:18:18 +0200 Subject: [PATCH 11/52] make sure tall images fit --- client/components/codeEditor/codeEditor.less | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 3594742fd..074d1e265 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -162,8 +162,6 @@ &::before, &::after { content:""; - width:100px; - height:100px; position:absolute; bottom:0; left:0; @@ -176,14 +174,20 @@ } &::before{ + width:200px; + height:200px; background-color: #fff; border-radius:10px; border:3px solid grey; } &::after { + width:190px; + height:190px; + //padding of 5px + 3px border: 8px + translate:8px calc(100% + 8px); background-image: var(--preview-img); - background-size:80%; + background-size:contain; background-repeat:no-repeat; background-position:center; filter:drop-shadow(0 0 5px #0008); From a2e5688edeb3d44300bfe4cc2c8d927c529f04fe Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Tue, 28 Apr 2026 22:21:35 -0500 Subject: [PATCH 12/52] Add lazy loading to images --- shared/markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/markdown.js b/shared/markdown.js index d2a108e01..05a564254 100644 --- a/shared/markdown.js +++ b/shared/markdown.js @@ -83,7 +83,7 @@ renderer.image = function (token) { if(href === null) return text; - let out = `${text} Date: Tue, 28 Apr 2026 22:51:27 -0500 Subject: [PATCH 13/52] Update mustache-syntax.test.js --- tests/markdown/mustache-syntax.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index 95ca2f58d..378263e58 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -324,7 +324,7 @@ describe('Injection: When an injection tag follows an element', ()=>{ it('Renders an image element with injected style', function() { const source = '![alt text](https://i.imgur.com/hMna6G0.png){position:absolute}'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

alt text

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

alt text

'); }); it('Renders an element modified by only the first of two consecutive injections', function() { @@ -343,19 +343,19 @@ describe('Injection: When an injection tag follows an element', ()=>{ it('Renders an image with added attributes', function() { const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

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

homebrew mug

`); }); it('Renders an image with "=" in the url, and added attributes', function() { const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png?auth=12345&height=1024) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

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

homebrew mug

`); }); it('Renders an image and added attributes with "=" in the value, ', function() { const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

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

homebrew mug

`); }); }); From d04f401c90f944fd9ba561ee8e78946e2ff9effd Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 29 Apr 2026 23:36:58 -0500 Subject: [PATCH 14/52] Fix lazy loading Print issues When initiating the print dialog, it first grabs all img elements with `loading="lazy"` attribute, flips that to `eager`, and then waits for every image to load before resolving a promise and opening the Print dialog. --- shared/helpers.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/shared/helpers.js b/shared/helpers.js index d2a9c8b73..e8b5596b6 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -105,11 +105,25 @@ const splitTextStyleAndMetadata = (brew)=>{ if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : []; }; -const printCurrentBrew = ()=>{ +const printCurrentBrew = async ()=>{ if(window.typeof !== 'undefined') { + const iframeDoc = window.frames['BrewRenderer'].contentDocument; + + // get all img elements with lazy loading (currently only elements generated through MarkedJS) + const lazyImages = [...iframeDoc.querySelectorAll('img[loading="lazy"]')]; + lazyImages.forEach((img)=>{ img.loading = 'eager'; }); + + // waits for images to load before resolving promise and opening print dialog + await Promise.all( + lazyImages + .filter((img)=>!img.complete) + .map((img)=>new Promise((resolve)=>{ img.onload = resolve; img.onerror = resolve; })) + ); + window.frames['BrewRenderer'].contentWindow.print(); + //Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear - const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0); + const node = iframeDoc.getElementsByClassName('brewRenderer').item(0); node.style.display='none'; node.offsetHeight; // accessing this is enough to trigger a reflow node.style.display=''; From 9af2577c6e9282069774bff200955273575a530a Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 30 Apr 2026 21:05:08 -0500 Subject: [PATCH 15/52] Add print cycle events and loading msg Since the print cycle now loads all images not-yet-loaded (due to lazy loading), there can be a moment of time where it appears pressing Get PDF is doing nothing, depending on connection speed. To add a "loading" message, a custom event is fired at the start and end of the print cycle (before the print dialog comes up). --- client/homebrew/navbar/print.navitem.jsx | 20 ++++++++++-- shared/helpers.js | 39 ++++++++++++++---------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/client/homebrew/navbar/print.navitem.jsx b/client/homebrew/navbar/print.navitem.jsx index ea262cf03..e669214b3 100644 --- a/client/homebrew/navbar/print.navitem.jsx +++ b/client/homebrew/navbar/print.navitem.jsx @@ -1,9 +1,25 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import Nav from './nav.jsx'; import { printCurrentBrew } from '@shared/helpers.js'; export default function(){ + const [printing, setPrinting] = useState(false); + + // listen for print cycle events to display "loading" message since it can take some time. + useEffect(()=>{ + document.addEventListener('print:startprep', handlePrintStartPrep); + document.addEventListener('print:finishedprep', handlePrintPrepFinished); + return ()=>{ + document.removeEventListener('print:startprep', handlePrintStartPrep); + document.removeEventListener('print:finishedprep', handlePrintPrepFinished); + } + }, []); + + const handlePrintStartPrep = ()=>{ setPrinting(true); }; + + const handlePrintPrepFinished = ()=>{ setPrinting(false); }; + return - get PDF + {printing ? 'loading' : 'get PDF'} ; }; diff --git a/shared/helpers.js b/shared/helpers.js index e8b5596b6..eeb09daf4 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -107,26 +107,33 @@ const splitTextStyleAndMetadata = (brew)=>{ const printCurrentBrew = async ()=>{ if(window.typeof !== 'undefined') { - const iframeDoc = window.frames['BrewRenderer'].contentDocument; + // fire a custom event for the print cycle + document.dispatchEvent(new CustomEvent('print:startprep')); + try { + const iframeDoc = window.frames['BrewRenderer'].contentDocument; - // get all img elements with lazy loading (currently only elements generated through MarkedJS) - const lazyImages = [...iframeDoc.querySelectorAll('img[loading="lazy"]')]; - lazyImages.forEach((img)=>{ img.loading = 'eager'; }); + // get all img elements with lazy loading (currently only elements generated through MarkedJS) + const lazyImages = [...iframeDoc.querySelectorAll('img[loading="lazy"]')]; + lazyImages.forEach((img)=>{ img.loading = 'eager'; }); - // waits for images to load before resolving promise and opening print dialog - await Promise.all( - lazyImages - .filter((img)=>!img.complete) - .map((img)=>new Promise((resolve)=>{ img.onload = resolve; img.onerror = resolve; })) - ); + // waits for images to load before resolving promise and opening print dialog + await Promise.all( + lazyImages + .filter((img)=>!img.complete) + .map((img)=>new Promise((resolve)=>{ img.onload = resolve; img.onerror = resolve; })) + ); - window.frames['BrewRenderer'].contentWindow.print(); + window.frames['BrewRenderer'].contentWindow.print(); - //Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear - const node = iframeDoc.getElementsByClassName('brewRenderer').item(0); - node.style.display='none'; - node.offsetHeight; // accessing this is enough to trigger a reflow - node.style.display=''; + //Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear + const node = iframeDoc.getElementsByClassName('brewRenderer').item(0); + node.style.display='none'; + node.offsetHeight; // accessing this is enough to trigger a reflow + node.style.display=''; + } finally { + // when lazy load images have all been loaded, and the doc re-rendered for print preview, emit 'finished' event. + document.dispatchEvent(new CustomEvent('print:finishedprep')); + } } }; From 9935f54080f78a79cd2fe75c048e0038dc4cdf23 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 30 Apr 2026 21:11:33 -0500 Subject: [PATCH 16/52] Update variables.test.js --- tests/markdown/variables.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js index 884553703..ad23c87c1 100644 --- a/tests/markdown/variables.test.js +++ b/tests/markdown/variables.test.js @@ -315,21 +315,21 @@ describe('Normal Links and Images', ()=>{ const source = `![alt text](url)`; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` -

alt text

`.trimReturns()); +

alt text

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

An image alt text!

`.trimReturns()); +

An image alt text!

`.trimReturns()); }); it('Applies curly injectors to images', function() { const source = `![alt text](url){width:100px}`; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` -

alt text

`.trimReturns()); +

alt text

`.trimReturns()); }); it('Renders normal links', function() { @@ -438,25 +438,25 @@ describe('Regression Tests', ()=>{ it('Handle Extra spaces in image alt-text 1', function(){ const source='![ where is my image??](http://i.imgur.com/hMna6G0.png)'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered).toBe('

\"where

'); + expect(rendered).toBe('

\"where

'); }); it('Handle Extra spaces in image alt-text 2', function(){ const source='![where is my image??](http://i.imgur.com/hMna6G0.png)'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered).toBe('

\"where

'); + expect(rendered).toBe('

\"where

'); }); it('Handle Extra spaces in image alt-text 3', function(){ const source='![where is my image?? ](http://i.imgur.com/hMna6G0.png)'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered).toBe('

\"where

'); + expect(rendered).toBe('

\"where

'); }); it('Handle Extra spaces in image alt-text 4', function(){ const source='![where is my image??](http://i.imgur.com/hMna6G0.png){height=20%,width=20%}'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered).toBe('

\"where

'); + expect(rendered).toBe('

\"where

'); }); }); From e34c5f9b3235db484e61c5774b1593a0c58eb1b8 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 30 Apr 2026 22:28:04 -0500 Subject: [PATCH 17/52] Add font files, remove google fonts references Adds a `shared/naturalcrit/styles/fonts` directory, and Open Sans font files for the 4 weights we were pulling from Google Fonts. --- client/homebrew/brewRenderer/brewRenderer.jsx | 1 - index.html | 4 -- shared/naturalcrit/styles/core.less | 1 + shared/naturalcrit/styles/fonts/fonts.css | 36 ++++++++++++++++++ .../fonts/open-sans-latin-300-normal.woff2 | Bin 0 -> 18660 bytes .../fonts/open-sans-latin-400-normal.woff2 | Bin 0 -> 18640 bytes .../fonts/open-sans-latin-600-normal.woff2 | Bin 0 -> 18620 bytes .../fonts/open-sans-latin-700-normal.woff2 | Bin 0 -> 18204 bytes 8 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 shared/naturalcrit/styles/fonts/fonts.css create mode 100644 shared/naturalcrit/styles/fonts/open-sans-latin-300-normal.woff2 create mode 100644 shared/naturalcrit/styles/fonts/open-sans-latin-400-normal.woff2 create mode 100644 shared/naturalcrit/styles/fonts/open-sans-latin-600-normal.woff2 create mode 100644 shared/naturalcrit/styles/fonts/open-sans-latin-700-normal.woff2 diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 202c1a375..26834c84e 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -29,7 +29,6 @@ const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState'; const INITIAL_CONTENT = dedent` - diff --git a/index.html b/index.html index fad6fd43a..d6bd3157d 100644 --- a/index.html +++ b/index.html @@ -5,10 +5,6 @@ - diff --git a/shared/naturalcrit/styles/core.less b/shared/naturalcrit/styles/core.less index 3ef75144d..64cd81396 100644 --- a/shared/naturalcrit/styles/core.less +++ b/shared/naturalcrit/styles/core.less @@ -12,6 +12,7 @@ font-family : 'CodeBold'; src : url('./CODE Bold.otf') format('opentype'); } +@import './fonts/fonts.css'; html,body, #reactRoot { height : 100vh; min-height : 100vh; diff --git a/shared/naturalcrit/styles/fonts/fonts.css b/shared/naturalcrit/styles/fonts/fonts.css new file mode 100644 index 000000000..5bb10a3cb --- /dev/null +++ b/shared/naturalcrit/styles/fonts/fonts.css @@ -0,0 +1,36 @@ +/* open-sans-latin-wght-normal */ +@font-face { + font-family : 'Open Sans'; + font-style : normal; + font-weight : 300; + src : url('open-sans-latin-300-normal.woff2') format('woff2'); + font-display : swap; + unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + +@font-face { + font-family : 'Open Sans'; + font-style : normal; + font-weight : normal; + src : url('open-sans-latin-400-normal.woff2') format('woff2'); + font-display : swap; + unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + +@font-face { + font-family : 'Open Sans'; + font-style : normal; + font-weight : 600; + src : url('open-sans-latin-600-normal.woff2') format('woff2'); + font-display : swap; + unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + +@font-face { + font-family : 'Open Sans'; + font-style : normal; + font-weight : bold; + src : url('open-sans-latin-700-normal.woff2') format('woff2'); + font-display : swap; + unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} diff --git a/shared/naturalcrit/styles/fonts/open-sans-latin-300-normal.woff2 b/shared/naturalcrit/styles/fonts/open-sans-latin-300-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d6c8549e82d163a6d2bee504b7400f5bb86fc08d GIT binary patch literal 18660 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 HymTr6e|$ER literal 0 HcmV?d00001 diff --git a/shared/naturalcrit/styles/fonts/open-sans-latin-400-normal.woff2 b/shared/naturalcrit/styles/fonts/open-sans-latin-400-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e2d3fa4ea21557e7ee10e08b42a82dd3de54ea4f GIT binary patch literal 18640 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 Date: Thu, 30 Apr 2026 22:28:56 -0500 Subject: [PATCH 18/52] Move Code Bold/Light to fonts directory --- shared/naturalcrit/styles/core.less | 8 -------- .../naturalcrit/styles/{ => fonts}/CODE Bold.otf | Bin .../naturalcrit/styles/{ => fonts}/CODE Light.otf | Bin shared/naturalcrit/styles/fonts/fonts.css | 14 ++++++++++++++ 4 files changed, 14 insertions(+), 8 deletions(-) rename shared/naturalcrit/styles/{ => fonts}/CODE Bold.otf (100%) rename shared/naturalcrit/styles/{ => fonts}/CODE Light.otf (100%) diff --git a/shared/naturalcrit/styles/core.less b/shared/naturalcrit/styles/core.less index 64cd81396..3ff3e37fe 100644 --- a/shared/naturalcrit/styles/core.less +++ b/shared/naturalcrit/styles/core.less @@ -4,14 +4,6 @@ @import './animations.less'; @import './colors.less'; @import './tooltip.less'; -@font-face { - font-family : 'CodeLight'; - src : url('./CODE Light.otf') format('opentype'); -} -@font-face { - font-family : 'CodeBold'; - src : url('./CODE Bold.otf') format('opentype'); -} @import './fonts/fonts.css'; html,body, #reactRoot { height : 100vh; diff --git a/shared/naturalcrit/styles/CODE Bold.otf b/shared/naturalcrit/styles/fonts/CODE Bold.otf similarity index 100% rename from shared/naturalcrit/styles/CODE Bold.otf rename to shared/naturalcrit/styles/fonts/CODE Bold.otf diff --git a/shared/naturalcrit/styles/CODE Light.otf b/shared/naturalcrit/styles/fonts/CODE Light.otf similarity index 100% rename from shared/naturalcrit/styles/CODE Light.otf rename to shared/naturalcrit/styles/fonts/CODE Light.otf diff --git a/shared/naturalcrit/styles/fonts/fonts.css b/shared/naturalcrit/styles/fonts/fonts.css index 5bb10a3cb..355c854b2 100644 --- a/shared/naturalcrit/styles/fonts/fonts.css +++ b/shared/naturalcrit/styles/fonts/fonts.css @@ -34,3 +34,17 @@ font-display : swap; unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } + +@font-face { + font-family : 'CodeLight'; + font-style : normal; + src : url('./CODE Light.otf') format('opentype'); + font-display : block; +} + +@font-face { + font-family : 'CodeBold'; + font-style : normal; + src : url('./CODE Bold.otf') format('opentype'); + font-display : block; +} \ No newline at end of file From 137b6bc690a8809fc819ba9c02d699d104064cf6 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Fri, 1 May 2026 11:03:52 -0500 Subject: [PATCH 19/52] remove unused 300 & 600 font weights Only 400 and "800" are used in the codebase for Open Sans. And, we've only ever had up to 700 as available weights, so everything that is set to "800" is actually getting weight 700 (which looks better). --- shared/naturalcrit/styles/fonts/fonts.css | 22 ++++-------------- .../fonts/open-sans-latin-300-normal.woff2 | Bin 18660 -> 0 bytes .../fonts/open-sans-latin-600-normal.woff2 | Bin 18620 -> 0 bytes 3 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 shared/naturalcrit/styles/fonts/open-sans-latin-300-normal.woff2 delete mode 100644 shared/naturalcrit/styles/fonts/open-sans-latin-600-normal.woff2 diff --git a/shared/naturalcrit/styles/fonts/fonts.css b/shared/naturalcrit/styles/fonts/fonts.css index 355c854b2..f7c0c4371 100644 --- a/shared/naturalcrit/styles/fonts/fonts.css +++ b/shared/naturalcrit/styles/fonts/fonts.css @@ -1,12 +1,4 @@ /* open-sans-latin-wght-normal */ -@font-face { - font-family : 'Open Sans'; - font-style : normal; - font-weight : 300; - src : url('open-sans-latin-300-normal.woff2') format('woff2'); - font-display : swap; - unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; -} @font-face { font-family : 'Open Sans'; @@ -17,15 +9,11 @@ unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } -@font-face { - font-family : 'Open Sans'; - font-style : normal; - font-weight : 600; - src : url('open-sans-latin-600-normal.woff2') format('woff2'); - font-display : swap; - unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; -} - +/* +* Nowhere is font-weight: 700 actually used with Open Sans, everything is set to 800. +* But, 800 *is* too bold. And since we don't have an 800 font file, it's just using the +* 700 font file and it looks fine. Not sure it's worth changing everything to 700? +*/ @font-face { font-family : 'Open Sans'; font-style : normal; diff --git a/shared/naturalcrit/styles/fonts/open-sans-latin-300-normal.woff2 b/shared/naturalcrit/styles/fonts/open-sans-latin-300-normal.woff2 deleted file mode 100644 index d6c8549e82d163a6d2bee504b7400f5bb86fc08d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18660 zcmV)2K+L~)Pew8T0RR9107&Ej5&!@I0E-|107!cP0RR9100000000000000000000 z0000Qd>fB)9ENBHU_Vn-K~!1*f+z@qd4 z4hM-J422&XfO;3u+on@+JHWGC_ZAUFxt!He1RDns*mvpd|Nrkv<&a8hpHv6q9)d}R zqcpmz{0bg(bzUf#cOo+o3xY)Y&bM;XeygUb-y6BCD5F&Sui@L!q@|!E9Su903NIAB zYWQZqosYt!u8(Gqq=O!KqLQ;7*ikJANrJ`GY*zR>=vc#bxi`M-jxt-AtfRk^RHU(K zv7Xop4kWSI&-`ZFAX|b+v|i2(rTy5JsOZC}LHU=*FVdY!!hc>qlntj@D>wtk`KY<@ zMgBeXr>m>yy)~V|j?4^WjJB}L3$Ne*|2gfc z3{Zi7a$ku_HLm14V`k;fjG1%GymNjjahej2FuUd`AA4xrj6Wc0_f`<@R6CAt=9RwFkA(X+h?yNKA%m#E%m7WH>{cWvB4wT!6RieOp1tq5mQ74&!*Rz^jOAL$2P3O z3to}YA2rBneKcO&p1WNQ71+YY{|>$ct1^7dc30*IV+S(25b6_& znX>;fM5Bp}C(hVnjv?C67^|h8_#86bS*QE^NzK&diYR`qHJ}J)TW{&6bbY#}vhu7s znUhgOEU_fUjL{-Az0}`EK*P+X#IRMjb?ttdwf#B5u!z41Kv60mUBpgHcPM&; ztE7A+YTluiIMf9*Xw<3w3)gj2G5p{4fAM)_AFqaT3dyD&Eoel7Ug`eIxf6$GyI1DQ z?hMC&`K#air62pQuY0>JebyUZwbZj7_aOiM;BI%i*|jcrp>r&DvJ)L|s)*8r=+uO>f23yN&^2+kEdJ?(+td&2%I6mjwe4Y>Uc3$;%Ud+=*=6WvYLQdys_UFNT z=-=6%jaluNS(^Eom1!x+gk&To@fn?ARou`Z>Py|J<8roXQK@O1dWCK1ZmPPRq8z~f z>zDfOyAAzFU(=_jysw+OTn_X+J+;;`i?!r!9oD3VO>Jl|-A`L-{c@EmHd1-YuyaFG zDZhy=Iwe!=a*+yAK;D+Ow;Sq89fq9Fe&U7Y_~AnuCd+lG?RLYl3Qiciu+0-&LtO5b8>^X|#ah1S}QRF5}<#?|XUJuq4o(4JYVt^<%*#}XxImEYf!!^bS_6J|yN7hvK> z!XZ-92shcabrYJF#+#dm0PS{u2ex-z#LW?ZT=9D-1uraa_y*B+BdZWHJy0)kMxDp% z+3r-Y(;$WWh|kOK4y5370j1Z|2=531DRP{egFy z?H~R}GOMgIxUKbN)|`pb9*56*(7V^d$|U{((|E{2uiyKS38NZ0G;R|)a>5wjFA%eB zT0*u_HPq3tYv-=2bCD9L=j%!V@xqM{=u07yj}@s6MN5$tf$GFE7aUWSE}a~;qtT-U#dOoosBb3|`Vii577jj?!H@+vwN&*(3mqD}WOQg$ z{F*k&3%jlU(;HIfl|ykO23XF>TG8L05P6I&`Q7e)e6>#wo`9lM&jJ{N&BzHX1<+EN zO52K0u#(DUGE@x35g;O)j3J&3SMMz@F&*5X;r6nSNBs!PF_w$4zjx4bOF_=s?TCXBUM!Jog z4`k=zMqPT6=`pi2nPHPrayg14ffmi7=-pMDW59$NC2Do%O{7qThTZl<_9<7R&8$7` zqb&IviLi&1Zwcp6z$Z*Lyyt>(lM|SP$Xs;+QD=jH5DlEv~mzSH9ot2c#42d{B zO_Z9FEEFX1d5PQv4x1IvWW>>F)L2RknM5SuaTqizIw~>(86FlIVr??ZKRr2Kp2Q?B zxUoK2Q8h76j^62t5640lQ11v1c!D2^8;qsZw9W;~4j$aLP9ImSv^v)gNns`ukh?E8 znDAhf3AOYBSQng*Okd|cw=oofim71KbEZ_rtUWqj20DRQlnHQ9>x&VXjRw4pxV>^w z{9NLI4~$9Aj%A%IiW<&_rg>wnNf%3~11hM@V4>$M-{KNclnKm*P3R^EfGkcXOiI|m zUzi(|!&F3&mKQ_Op$0Fo9Y@B(UdINyji(UG_5@?TpV-x^k)R%P{P9rq$(JLUlo`N$ zIV!&pQjym~To~&lM)4tnsvsPMOpDo*>=}o2U$CW*Q-b6t2nVVX;aSmSt09CbmJJee z%tbtHMca^q9;G$e`20_Sl2w!7VBa$t z&>?$I+vR4TSQ=R5yf^{Nob`E^uL`kAC@T`&9}2$}9M;|3S_R?VF61;;jK$F^s7nG4 z`i#f3L0cRkFpIfIvu#HzGGgsB(B=xPz3Q~zTXJxF7h%E?)}0)#Tigu3eyIXslGJ4U zfIqM$?U0TTFF#a;V{)Y4u?y`_C0SS(z+GmCYMV~Xv__ea+a4df)A4vz9LP?k0O26z zVH3H79P*rfQsL94P%Ln{vEf|&VJ0(97kZ@30pl6AP)8*ywPt6)xePo7slc$QJ|J!p zK(E$$-C${n_E96a?Wkbj#6rqn7zUpzyPNQyzo%)Mp|UCE57aIaLdQD zhxUwvt~{m(z+u6`dZ`K`?0Z(4=HBzb#PfJK6UIBzBS4+k%S(t)INNc6Kt~Ig zKRUg3u06vx2+-v^?!}~4NsIxdd{a^q5$)afLeDYk95ofQKiC7o9G$S(J>p>l!?;u# z5^jFrbGv@zp%91%sr$a*_X1nC>j%+^oPmxowA6zDGUAnb1(SvQ-D@^LrS%p{WRLu^ zOENB`q8F<9bSeeYP0r=YcAaZ@g||?dWA+D&G;EGsB3EOp4zYtG-y>Rj?c{QlX%tOt=^ZF(vuQ)X5}0EEc{lm!GX^L<5Y{Fo{0l*%^LsZW z8XpN&gL52FL3(~xSG;xQs~|mgD1goO5f6+I5u#k3$8Kqxqq5(-7t}?!Vo$^=-6zyq z(0O8y9g2&NuI|(ln20T<>;XQZ*!@w5%c0f-5iOH9Cj#h+E`vLIi0uj|(+B^G?er@8 z3JS32gw}4&k7qBeGo89CSK$rB_CAzC3Xw1NOLqbe4lDH;-Xx|n2{#d0+@7)`O_P7A zH9J#9q_hZdn zu!^1vw9j*5k8E~R9l5MtoqZFT%D2F!^ncKkqF>Iv3;Vy}NT}c4cYKoTd@EV;?!Dl% zwV~Gqc#-6Z3;8~6I+Vy-bZ}{{JB(B8_!7ph$i!fr8Sib938$1iGLP(1##mz%2!UH+ zO792Yg|dCad=&;hr}I8$s>Y;Oy#Cx~2C##VtTn17pX>J=q(fkuORJlSOSsBkI6mmQ{~UkmxG@Z4-j# z&gBzp2$I#1kX$HCqLThfJ|XG%p$&G*c7o+@%-Xl)32r;|Ua#)_6;QVXp8mue_BQ}+ zU^5^mf?bVq$Lbk9#g09W;VWKJ20X6H9?&ZlHrB%%e; zllPu7B5IAF^#;A&=_L|wX&~CgZN!F3jN2D)0p=S@bJg`JWItN*bOB*b@lq;O(|Q|H z(4Y!b-hg@o7vkXmUEpV&OXG}Wvp66k3mRmAa6S<$+7AH;W?V#@F8R03VG2xG*6naWj!rmD*g4SqSNXQ zA;LuSjJ!=egtTyYB08ls&-%BK3+;TP=#&a>x#cxW|7uVMnF_VZ?TtT{Mx=#j%I1Z^ z;}cMvk%<#RBBd2nQ-(d0c!qx09gs0Xd9)NY7gc9Ij61r|tFzc&PzBW~biZRvO-+~i z4&>xp!ZGv*yyf%P6Fow0c4>09-Nl&#k+{9nwnI$N9N*SFO(kgnXe ztEhq?GXl2=^pUS0TN{1pewc1hW;^(EB#Sxl@k*5LObLYN(!+g}x7b3Gr2r+Az$Pp+ zqj-}&y@sx|$C0=AA7zf+D*}uTCigStQ4z{we24;mAXG9X$|eb9pSYO}M~Kk=pq>su zIx`3IOwyM@)4Yws0qA9hl@CR7(u(FlBl(iNot5+nAY;n6__kV6Ew@6#!Kk)Rf!YJj zXnBS8$~DNCm$tfcMrV~}ZFQNDSeHSKX73;D3-~Bgt^8*g$yr(jyz(!|N&%#;%=;Ei z`(DXdT}{|4;|KNE+g7W|&RQgF|353qn%^&&6rVy4 zF4A9f7!yNsxXhu+v16O|95`}hLp3>w<-A~>9IUfRifA;T@rZ|G9{elpTV#h$fSc1Q zx!+x)K?Pa@3iJ0Og*s4mVI%Gb!71`f{|wMQ^aGvfJJ03h(i5AR_coqNpZVhWzj0zr zP|fobp}9Ke9g^WZEw38*FuR@&=N%KErf&=0K<3k0E|XkhfR9O-9FS|QLYLuq&=9#k z57LQ_)+=xlih-G^P$eiy=ylxJD9!-DdDpU%=H91C!ooDkwdHlgRSp+Q+g7pG*;E4$gU z3D4G*J)~tC__&!d`>SjJG}@ECvY{fh>ec2N-rqfL*H59N0Q5unlh*A|A5yPF!o#bV z0RwW&Jqk&;mh^I2lr(0*&-^~HV=~P_fIM(unq4%Bnhs3?EGJvUMeT(QHaCDS!g11Z z&pWe^kGbHyosNG559W8y6$d0LWAC^Zx?z}E$RGk?_7f0J0>c3N*6|!fp2=v(&W?M`z6J#;i9mJIkcBO>q?pg#l7yrQ zKT-0PZ%Y{A0Lr`C3P_*!f^CZ!4B}THw(Y?mhTu->G%lKBSueZ7kSDzNaS7! z^rpu;aoqj4lj)+~&v&{`ul+Sm3^KF$XEIVx#3Gf*DA9h2k|^4(9(HgQt`;AMY(Vd2 z!k1N;gHftCPtuqt9K5~tbJo#9LjZVbLd?u&qNf3&dZWKC&&)2m_>HCB~8W&$@hDHH;p@Pzvp!uUqf2hujSM< z_r-|-6^_|rAn0jt(zEADJ3`O#(4ixZpNY+*rN@~W2+9sJkXUKj^Y(lo1%5qOkT%#3bhIe+lJ9?d6{od|w%5k1{ zTyK77dv)I|Fmgy=c&VswG9=|&63jidL*hvpdSx380>CbEmvTM2ps zbTS0b7$Msuq|w@DHwB)TLayBLY!J-jjN>R>_%rU1jhbGbGkY`ZkDYYG^&V-n>YBI} zj9y_5I(+2D9IWk)h~OVOuItd2|A-xx zxF`gJAK>F=GY!X7SO$o9%4>!p6p&4TmHIU%3k`8yCYxl3)d2#=uUIj&;l8${ z9g_Yo6d7L5moC^SEF!JA0j=MB2GV<45ZW6xs9BsrmC@%$-q(qDH^260Q9-xO)jC^b zM%F|Xs$~yg68Q{}C3sH=4fAu!^d=M-RN*vF0Apa%ic4ZNZz;2=Kx_Cqgr+#;)|Y?s z2EcGl6{WRyU*LzqQ|@e-+dyJGf8(6-;8JU*pAtSh=aAcMc;AetPx$yrOt<_NICX#- z8n3mex@ho3>y70A{$?&@o}r-&e=VRkz2F>L;-s+h+eplni?zb{{tN0Z3)(qUYxi-$ z0>m6(4iH1&7Tn~4j1oYo4({j;dLCB0`gt{P(cL+Vo7UPMZ9raDAraEFfWI;G5&goXu6lNul z&uX4Qrz%vVt72uwEm072LX_Dd^H5o<9P7g|jFP1OyL0oqCRHkw`|@+b;q;PVrYWjO z1utLZ8ODu7hjEG!*818Pnj79dc-r>1zNY!Zqerc8s!p9D*ni-l;Z#dR_ z=-?I~i`SP=NiWOf5}EZevGs_K)?`+J*p8XEFgtLLa|n2;AtR07vD9|)|6#aCnL&IW zk0CDAM)>Tw?K1<1mx3809w)cdIM8QgB*?qWh@H!4NJ@3#emkr(cIzIfE!O0fen`P= ztxb$yJQ{;``TXojlvQ8TzM`HcB=qri0QAtI+Nz#D1QayRLi@zXgqV!$Cf^$H2(7aw zXv?<{r;aD^YbV2|1a*T4>f)y1*g%hv6Xoza^U!@#I%h(hh27{l%H`)($%AzL@Ej(p zrJ5E4Qk>9UTsWAUe*CpyHuHiV&WeS#zU?Zwz9rqCDl9+kSKb@bCz=mQtmRcsRy9VI zo8vO|NVb)&i+72VKQ(z#p8}>tlVi62t?F+ zlO;Y@-CT6-y{J#TY9C;k5NxyI#xGnWCXVC`Y)a2!TXjBndLKMS=h`cuuhfAQCJ#ER zu`YKsTnI1lVC|$(BbjQRlW)3nycJ(7Z6-1g`X!Yf<0WiXHuLYC&8o|qXJ*a#zJA<4 zB;7tXIC5+}W1JRS4TnLx2=(=`COFF*WCZ;%(sEOvX~DUWw~XR;2x8D>b!-|`Xfq(B zv~lyYXRlBzYPV>qlVSm7e~*x3iLx}~e~>C|Km?}<0P zxjqW7lot4`L;kl`Ykc52^Xf_Wp!DME@bLPCWITCn4Z`1|3tubB zl_zm~&%S<-UDY7-4;`Rzhhg_YqfnGS#d^}Te+N>ArQp}>leHB4DZ7KrcK8Gy_E$q^ zXf9tWXt9jlN{|fVB+>gscbirfCR`aSXkOtT^*g#7l4U_N%tbO0t`VMWhI1;mw6Ca~ z&;<-SF)EX003pB?n%sdv`Kqwu-KV0aO?KBGCkr24S(Fhq+{|45Q=S?H^DPdr#S@u2al{Vb70UBuGtX%Ri@0c~t3L1lkj#+@P@o#=iMqo$& zp#$Re+P8iK1O7=Lu2ksi*4|PBB)TF(5ugtyfOy@6wq)BxYmzMKyKw>Wx+(2wN(;3` z*YWF9{hma-54PoR{AUMH@9!TbM4fn(b=@w`&N`NEcgl`)+Hkbp_T+;mh+fmV{8H{O_mk=FCu+`^4)HzDD0F?m(6xR%F>P5crb#|960k` zI57A_*nbwIHS(|>W3_hYvgtxAIs)|Ol*IPi$nxr!p>&(mUZ9vAD?f*_pdbiAsvvjf zngB6eCEE6?enKU4AF-4lgSYB01PHsz#xCv0dtBW}YV^KteTcSn z`gMe4sx<}OUB{t<8#{Bda+C7LxOpK{zLrAf&rbBsT`Mz!Ic1lW%swGE#597$24A>Q zMi=d~(`ExXWeF5~+8DObHpaLa!KJttlCyILui{I2t5zf{2$^I#10iyIeQDV=22s)* zCA28ATVW)$7)B-n;)J_2L)u+Uw43zsM9IgW`z951!7|ec)Ayf0zg4%%=5Exig2G=& z%{HY8>Mx-{U*22Ih^&ukYtxH1BJbAw?xmSxDb@QSWox#g`_*1=xVqNQ-N@4{<#Y6g zqIk&}G-DSW6ucX`r14$0zk5)5Su;W%u`7U)2O{dmz1C<|F^JBXosv$8 z-u*hhCaD(JiA5W$V8agZys^kQy~vHb$9TRfO&9q)!aaDGCw(Qtov>$e#G!|kseg|Q2;{D|cM=b-Z-X|5GU1DrY<-KCcfh-ccjtZ2>1Smjx@)5ao-rVS z4&Dy0Zj+vn-;fLRetEa`^iX|rRkuX_g?EIrXD}n!nr4>NpPo$XntgJsRy=|Jln%SSd}hnXO*&d82sJRMi>wiT;+3x~u^zZn>7k zfzOPAMk@@)fw?qU`C3`|SXua3S^0vD-`T@et4Qx&Ln6^MCLosL0Ag8+Z*GdeTqxhl z*8=Sw($r|z(g>GrmqE&&@JBwKx|uV1<*FdN+h$6djyY{=kZ}JpNKnPF@rM={Pu*qR z?5L^}9hhF&C}rQ|_!p-5M{-)QsL8Sn4h*X{r#KiBQ|dvH^e37mDA_TWgw{5$_#-Ee zZ;?DQzk2x^3F-e7HWe?j<6FE9;QeO#e%sk?3z)35Ido1NY`b7a{NLU3uI{V#y#L+S zhZ`_AhBFIGMlw;i>Z&4c4CIuqZTt(eVdJm9)Yn`R{ytw)AK&O5$3CcJEtl(YECGi1g_H&t6V_wY0)x?IKi^)v3yG zWoT5E7cR-JgHBF>kyI`!)>GI;KKgDajF^ndqal@|Ip``IX|cG6G1?+x1%}}({NhYl zX8VIk$&8XKO>^<176?NK;eHQPb3~f3TMRQ;amZ262s|HnUXrQa;E+jr zMR+AP1ZKq~Db3|sBpBd}6+bvNnvmre1ttnRAv8j9dJxufd5P4)H}k?e1TqaH!q>sE zDsbzQqg4E@VC(teI{j+6q&Em#!i+#U#dGa3Bn;QiiHV71;){BN(n&<%ftME}GAt2a z9Iv6w0^y2zQ4yZ>bbAueZzI(s1A0laatNG!73E1sM)C+ANV(M?3nqp~c{0-WkV%r> zpn~yCB*rnGYlpj)y(1G9#UPa2R2zAWJzyC7d*^s%v;VM$L<-G9Oy9+sczR`dv@&0q{rWEt;1cO zmQt$}{4_Mo`?6VAsXnJ=*LIG35E@`;LHMuA65mkYH|cNy zcR-b5of8}(b)-Khh3`s?a4%-4UBg}TE~hL`&@5-Kg*cQ9wRB{)w^B=!4_^TY&N z&grT-PPaMSz#Ma--HcdKLne76f(xBU#x&!DVbl~Fhg}tJi8PDllZvl&oDB*v05R)2Oe4C9cRoiZ4V)cDD03Be6cshgk}_KLYH8Ikd&_A3=_Oo77whonF3KU zwfGyd4O^J1ZynNG&0U#N>%GTlm@zZ z0hE^QuyOCjtupSpwUk0sjv)mSp~TY*Xgj zzAY^O;5RpZ=SYumem2xFza3uw$urPVIFlwyN;(;|i+Ydm9T`BkamK-d5ajy30(rKa zR}==0aAWAdHi9cjOs){(ESQi3Q`{_BcbZR<=ePD+eKhw){C~OXY zok0Q;v{dn`$_6Ykxhg!mf)W!|UWJT_sf>s&CsU#;D#OW9oXk8XCo7B1OUvRg)AB$< z!V$vkAL!Xd(&7wpW|3q7h2vqtKYm6=vQqXkJ&>_vB8+d#U+XS&jn1zr;)i&PMcL8O zC)T%iYnNXstd+0{q@+NQ0#<{@CFYFO_JtRNtMCB^MP@;nnd}&17Ams{StJlpu_19$ z*if-;aeimDlYt@8OrszejUnJrDPbC!ammEA)!hN#VKBVfjN%y*xy+f9-i)Lx>H{7V zZmn!UZp29BkygD5Z&U{^8zMkup6WQo-?szMBJBf%8-3mjkfQAgWtkRNa?0KVR@( z;Eef)={R@A7R2d{hF-b~DyN1f$%O}h{~0BUM@A(RU`e)=jXvVkQM6T!^Je$G9`O$^ z24~~{D>8*=u>aVMVq|d=iEK`{Y0e)lB^A_Wun4520J%k|iG-Y6JdCvH`l$LZt$sd4 zW{Lm1<6u5#EvSZhi`gcmC+9Kt7nKnkti#L?V+B-;BSCmWMNE=uN?d2h@{Xyp7(nd1 z&*f6AZ%8L-1Gc%eT**MhVfndV@piw#`}USY(l`DC1O9JUd+*G@q|p)D$Xqf-h1f)E ziuw={iU__^sjE|KvZJ?!e8u&v`TO$oFXZPf+P!>l-+d!!u_(IA{PTek{#ip|PWjU@ zEUMYu(&e3$ob-+GO4%RkBJPQ0GR^tRJz>9saeK9P26#;SB`;;xLNLy>!N0+Yd*zkC z4Y_d_HPVZ~xU{Kd*3)Ji_!*yNV(cm|A6j_Lf}gmipWo78<5YQ zpLjn_YcP$0($J7DQ#yYrM3iqrG$rr~xz7m8E5sHibu>uvw4BNx?9WdKiN7mC>~V)~K= zOSM8xbHbs|EA8Qza}!v#Kf{dsCin_hn+`Ny$w62ondQSDfO|!s=HAZ17lsgb(@fRz#)d0S zK|!82YFkJvl#J3;aYRLzd*;iHs7H_5A|OZZ#@1bmtsIAcXxYrL`qsDXMp9Aw`U4b+`CYOsxYT<_Epc%*WV9n3KnldjIhvPLtCe48-zQ;*2)3yy#o`f!skiwPEj`S_ePsN zfkh}I%s(_LO}VBd>wtaG{K;K=;rB!A*Xu<2@OkyRBibVH+EH=^#VcfJ3Eu1{}sHnh@Di@G&VMroy0C`Pu9piZ?NM( zzmolc@r)t9f~|3t<;f~Oj}7Hoikd(b*mkY*Uigmi2bFdYk*igT`|nyQA8|&O!zUt&wY3*RRID2gwPjY%wl*O5ncOBF&oB=Z<|eXZQj$b2Shd?n zO%9msD8cKpjGv$aVY)32AnlATNrQ+LO-zV3VVK3G;`j$Dien3X;QSCD8zLH(fwBmX zk6<=uGPbXx-J#*mp}g3*_ylsKFP_0O+S!PvMF%0MXdIZRPap)ssgb&{ms;K@$GuOE zc@K>F&;GmD`B0F{bQI2t39nl181Tvhr`X-a%)!m__jthW?DcjHfxAZd2Bo5Xz`MiH z{=S}A|FB4`uct5CS6BbXDdGi#r6plRN-Z{eIIqI_TteA_K>0&nY&#grGt@=E@PkuD>4}U4p1xN3jo2Ct zY9PPD`8=m$e-O_)NO=*H8KSx;rGg5dP!KD=7YCCHnj6YX`uhqZC?XMu882X8&5tJ9 z1L>T^@HWurz&NTO#OKurE*ASK3#eWFgxV_y1wA_I1{ST&GP1?fbn&V;c=a(Ey|p?u zd0~l+a{2rMI~TgLJ|2eI4uBp!R9n^C2Zw@Y(LTV*uXRx9_vjv4YfS{i>)KdJ2BH`< za7>_kb1A&u9QnZ1_=$`q>AF5*PSfPkay`ZIy*tQHKlYkGlXG@V{Sr2&ox9u1re8%* zOmEuZ5MEOxSqq^CIEUfoeNubw{;H9qUDc zsgcJ#ba>e+k3-HZmb1XjobrD2xNq=CDHvTDlNHNQqt@=#q6=5w5Nq(;a)=z*>=T61 ztlOfdOk@ZsojpPh^eY+)W`B-ivkR7^*d56+=(1Wi6)Ldl|0+mh=IqrsgpfQGN~w(h zLdOkbQV3-|VcB0vkSZcN9g{y&*itp~g^%rZ1JWY~oz>Zwzt~ZoN9xWbT4V~yYvgTb zfv3VJneB7pLi^|Z)&4cz2d&1K(DEvRIG{MU6i5rWuF(~U>KeR>_RmAM zX*D*AXZwWlQu`cG?Vpc5O{@D2#jojgI4J(@6Jn)(j)Vt4k1hyNVLT0GJsi3v>Iow# zFg4T_fQKlpZe*rPUMx`zvI!vC=S0@Q&lz~02E0C?z0m598BMSF97`_ohTw)%x9~|I zg1y-IoQphK=ug>c?jhlUhMaw|xZRr(2ze69q^8#lx|0!P%%-o$0<37!X>_oy$>NVso9LJBSY<>ZJ(NZekNee|SxgSj&5$|JyF~i4CaU z=z8eITSl~AUIS3Wp479UOmogp6L++EX4?sJJr?>e$=ePx0d-Tt{|1|Krb|t)|EDy) z=*JpYPnU^W8Ug@ezXFM35(WhpHuu)O1jyixi0nNt&v{2M|3ZMni zWq%8eg}fJ4ILGzujoEbA$ONMp29cRfgpC+lWcuPcjbJU7^pKLWxTIETS|s%WvZIyi z81Ee&9L=TA8I1Ep$SX)E4f&$ALKjJ{bn}(w`gPC z`}zM{_;qitbG^~eLbE)3m&^i?-oIiNYmzbM62&TL`gW+&@t%1mh zoRP5Ra8aGZPeguO?4>|u)=4ZW3T8eKdO**Xboaefi7~B%ubfwS+*LODb$)%-Np?YD z^;Pi*zRmTDt=A^(ipcX2vd|dsV{~K7Drew3@@PR66FYH3&lVu|)2iL-cwFz5^|PT^ zSM0@R>@_erkNl0*ED9)R-_2cmoEmkZqF|NiAg3*;FdXn~&T#$fpY`@L4$8?gH`04} z_O+T_{Qv|Dk}jk;peUfHLxj4ki62$lA+rF381d=bcl86^L*XAUnM; zDzdKldd=+MR+fa9N)q3vCy9Q15^77~kVl>%N-tVk4c1D=c+psS5R`O>)FI1$)+LI3 zzW^X_SA0#TtI7SLRGQ5uJS1LkjXKBV6Ggo9c~){R$VB9+@o|zrMYx94aE7EEP}1F$ zeyCs#WPs7WPr5P(E-_Q|M)x8xLrJP)&BB}m6UXO`6MGB55Rf{+QO%?dJbO z<5X+fHci0>vAGp+o^Yl!JR{6pwb@=UD_$FE! zQPKU5oGGmrY zvs|h6oNViPWwte+u(pW}gM=mp$yy%?cH!sY9om#}s8=h&0cT(rqAnOsDLTa_@#>D5 zdJJ%1IfHJ$jzm0exJuZ`qUB|*76RD?isZG{3`RQis*y(#;y^176+IrgbiaiSJQ-SA z61nsSnPX5}1fdlO5XiN}DhuW{qQspr41fmVjnj&;UokkFtm+U1vLg=q=K>mvszoBO z#veg_1>rD*{M6$CZcmOS0HEPXJy?bm$UE3}UfuB;-N-3~ey3>%M3V2nZBx9p0X(3jH zcc_*{(D>X#TT0mSc2edJ7!-Yb!5P0>;1RV z{q8o|c8jK}#)GHvwiI4PMCpxkAvCT!r3^j_D7ObVRMwhRtBmVxzTn$-qJXaZZQ`P8 zl(SQDRG_3C%*sxzCnNc#QIN{0@wU;t@F3aOT*TVMA0!YV=yeh*3q#*StEj=%z#F-QYxxReP*q?lvY)*b zJjOV!fLMm>i~`Fk{XrD*eq*_E577YyH7Bb;q>S;xkzG*A#`w#s&DubiY^xGnE;kKD zZRW1M4(pBTtcA{D>j?lmP_O_B4sNiCv+H%1fwDORyi0bi!@2RImC9q_)!S?g$B4O9 z4$MUdV=s&J(7Cp%b;!9zO{23OD>Y_NHIOlb zh@cWY{$Eu(?@9jfNsQ*{2o>fxFzYxr^*v{iq01gtR2B8E`*#MLfr(+(6^duY;}K;* zO8Y(lQN0!0l!{{Q;)$d+Mg`$OE6yUT9y~Lk)OZbJu;g*{XD?);fPwjB&s7JP-8dL` zr1=YHK>DOE!@Dn!hoPY~VhC0{#{{Z(gcwPQ*K?W<-dyuP_>R0#wJ0gQ*~GDHyj=rF zDl5)DBWB1!PHP(7qnIXDcRM`<@v{Yz(+SF2(MYE9P`O;2RqewjDyrCmwR znK=en$O6n}8>vzYUeg}@>nwv%8tS&qfCW^geNT+rTUR950*Y()Y{KbD>mk*Qx5Xet1v}da3fO1GhnS z?zYx>rg`3%xH<3ys-z5M8A~Uo@ zGk(kt3Eq{1=Ar?y2u}Cj3B!eZVW1^sbD3u~HLR)EXNbOf$XfJT-=vin_u`w(^G==%!vM$H?cx7G`k_IAm9>-NyiA_g#zbO0^bFP zCj9RRYr1RHL3wX2GA7=Vj%G8L)@#JgI*lSusg~aO_Ll(4@ttH$y@~L{n@*wI#T8%V zzH;v(AYc1T-JjSoJwCE9G-8UAh&uY#N>KYgo1uFSY7<(5x1t8Y)$tz4r*u1f(-BpY zQC8^s`8*7v^0xiQ*XWQ!*Q#KbenYYrQ%jbMTgN{$nw_=MZyq$;Jm|t!E8qtdL4LA! zd=!cge+=Ox@=*v$a~~p*u%Ah1VFR0r^hX9Gvzv9dg$iV3{en!6RwOwW$jEuN0*l_= z%hLzK=UKRdP&PahE0_qIhjrmH8v&AM5Bqh87UcQzPYBxM*MnH zn*PkM-(W2S7{B&kFd%P&Ri;iF%X^CxbZP0+w>y4hP}gWIJ3`z$SOTpUu<<&dUPiAM5{NStBBgZdNE>Mp70^sa$Yl`Wpvg{}t}^L(&ve+} zZGbZH-oGfV;UbcRD8%brOHOE#ABn$5iroZU*ItWF9&bdcr&*@RG*ax0dVc-K-3>5X zi$F(>;Q;(Nk*Z@lT}I)8>c|;B{x2OS+mD4l$$V~+0bL1aRdE5^8b=>xAuiEd2FB5R zWKxSW4FOlub6l~gskAhbqFjceDfDA+IIVi1KENpd#V03`$vLpPhKKR-Z*f9%H_Ogk z%&Qw+CdbadE}K#2?ucxoa5PO+W)o0CC_C7$>*eeyL|LIU=1t+#Q%&@oHnYQE7UN1Xx7ubo2VtI zLm2^}UU`o*U-CJH)^4NN1qc0cY1DysI#11@+exp%YywI`?4;JAi~vwC&~QJhGH$Ur z3#DK-i9~O~Y8!00gG6gd(+p^Hpg(MI($>~-l3;PM)Llzpm6QbkuBo=JBr@ACx>=K-Y;fO-C;dF#;vBGf`s7b2(Ts+=vBh)#wEqYWovtpxN8^A0l+1rm^lp^FbE0rdW2CCEX5#d$u&t>Op16o$tW){Gs`(5B;f z0e|iXMgQ><=A1iTI?CGj@zKXF$7h<+aShJrlLAORK}4h;f=P5mbr36y>clFj9^f#k z;;Hp9Pbm|a1VZg#EVerl#v)xs7mI`L-pXWP&|%#fa&hzU^6?8`bOswh=DQOv5Ie7&|C1sV9M;XTpHoL>=a(e^{0*4SRM5r*~kRn8i5)B0n0}BU_fQW=F z21P6?8oD?P%xplZGHcqsR<2%+L&gjyRi~6_Nq5v`RXXWmq}#Y3dh|JC$~k9Di^tN4 z?E;Ro&b#EIE!*BmaLr{`%;5U$fSaznA<;V@RN>=EA|#NkR|=6-Y0^oEWythamK@n~ z?UQFop91*`6?yNY<$TwyIk(+)-vf6XHc#q^Qx+`RRAHjfbIft8CAH|)rBG=coLt;I zynOru7@YzAv_XRX2?>jciit}|N=eJe%E>DzDk-a|s;O&eYH91}>ggNc8@}TQe&QDj zw2aAmH0pdT_H>=y&`Ub;J6OCG*UIHqcLY>cwI~#uP_j|(W8GzTW8Jl89pjIggG-!s zM@~pp)rEZnK@FjJxcCx6U*zXCl_GvdDrIrf;aauj|9`e|H7;q@#)SOk#H2|lld*F5 z3GCXp@`BDj@B+=l;hdErGIC6!NB!`SyZ|m9!9M~5>#A+tmvyx5;vY*MF0qT!Iaqtw z7`n6P_?WY?{VA?A%#U#4V1Bf*{^*>UoQORXGgk?`q>9wMw@k>#>0vCye1-OMYBou5D8%Qp8Z+-P za`fVIdCkrh&~olDv*N{OVvUk1h{~Y0)KkL&w%Hv zBg%(;;LWCZu{}Hnzc%5yme&HOLK?}0eF0dz0zNZtkVPyyfTa{iW&~VB*aw7s7BjqI ziUCZR*KHbl47$f+=(ZS?e7Jc%-SwLvAy3Ms-wVHSD{|7Vs?$KGvl}4|=WEYq`)l`R zXXRdb$=~dC&8D>yovuBZ9j-l?CFjkti}IvADF0X8MiAaNZ_Hola%Pff?a^#^?au6| z+$k^V%}!S=GrRuTK2G89=BOW(sy#&@I(QBI(%2*YkRN4`o2ubhb}bR{XZhWaAJAVP z;P(;vg~6*JM51E!MdPfX{;^qR{9Sx(j37Z64&DcnV<0Elx7-20AqEwW_=~v@thRh( HymTr6e|$ER diff --git a/shared/naturalcrit/styles/fonts/open-sans-latin-600-normal.woff2 b/shared/naturalcrit/styles/fonts/open-sans-latin-600-normal.woff2 deleted file mode 100644 index f48dda7fc49a8ac138da63129451bcb4af0dd2af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18620 zcmV(_K-9l?Pew8T0RR9107$$55&!@I0E*B607y{)0RR9100000000000000000000 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 Date: Fri, 1 May 2026 15:17:30 -0500 Subject: [PATCH 20/52] Fix font snippet "Open Sans" Previously it was using 'OpenSans' as the font name, when it's actually 'Open Sans'. --- client/homebrew/editor/snippetbar/snippetbar.less | 2 +- themes/V3/Blank/snippets.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index 37853ca75..0ee9e1ea3 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -184,7 +184,7 @@ content : 'ABC'; } - &.OpenSans {font-family : 'OpenSans';} + &.OpenSans {font-family : 'Open Sans';} &.CodeBold {font-family : 'CodeBold';} &.CodeLight {font-family : 'CodeLight';} &.ScalySansRemake {font-family : 'ScalySansRemake';} diff --git a/themes/V3/Blank/snippets.js b/themes/V3/Blank/snippets.js index 4e9ec1afb..f70efed26 100644 --- a/themes/V3/Blank/snippets.js +++ b/themes/V3/Blank/snippets.js @@ -820,7 +820,7 @@ export default [ { name : 'Open Sans', icon : 'font OpenSans', - gen : dedent`{{font-family:OpenSans Dummy Text}}` + gen : dedent`{{font-family:"Open Sans" Dummy Text}}` }, { name : 'Code Bold', From f5acb27c346d86f8508e53e9a6ea158ae601df11 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Tue, 5 May 2026 22:38:43 -0500 Subject: [PATCH 21/52] Revert "Fix font snippet "Open Sans"" This reverts commit 34d634a34979b292b30006e6a6092d36f1a77284. --- client/homebrew/editor/snippetbar/snippetbar.less | 2 +- themes/V3/Blank/snippets.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index 0ee9e1ea3..37853ca75 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -184,7 +184,7 @@ content : 'ABC'; } - &.OpenSans {font-family : 'Open Sans';} + &.OpenSans {font-family : 'OpenSans';} &.CodeBold {font-family : 'CodeBold';} &.CodeLight {font-family : 'CodeLight';} &.ScalySansRemake {font-family : 'ScalySansRemake';} diff --git a/themes/V3/Blank/snippets.js b/themes/V3/Blank/snippets.js index f70efed26..4e9ec1afb 100644 --- a/themes/V3/Blank/snippets.js +++ b/themes/V3/Blank/snippets.js @@ -820,7 +820,7 @@ export default [ { name : 'Open Sans', icon : 'font OpenSans', - gen : dedent`{{font-family:"Open Sans" Dummy Text}}` + gen : dedent`{{font-family:OpenSans Dummy Text}}` }, { name : 'Code Bold', From 793adafe3216a55d8ba5674d74ba2d0b69392982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:35:09 +0200 Subject: [PATCH 22/52] fix work on first load for images inside other nodes --- client/components/codeEditor/codeEditor.jsx | 16 ++++++++++++---- .../codeEditor/extensions/customHighlight.js | 1 - 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index ef28bf509..90ab1fde0 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -17,7 +17,17 @@ import { crosshairCursor, } from '@codemirror/view'; import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state'; -import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language'; +import { + foldAll as foldAllCmd, + unfoldAll as unfoldAllCmd, + foldGutter, + foldKeymap, + foldEffect, + foldState, + syntaxHighlighting, + syntaxTree, + ensureSyntaxTree +} from '@codemirror/language'; import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands'; import { languages } from '@codemirror/language-data'; import { css } from '@codemirror/lang-css'; @@ -41,7 +51,6 @@ import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; -import { syntaxTree } from '@codemirror/language'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; @@ -90,7 +99,7 @@ const createHighlightPlugin = (renderer, tab)=>{ let pageCount = 1; let snippetCount = 0; - const tree = syntaxTree(view.state); + const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state); tree.iterate({ enter : (node)=>{ if(node.name === 'Image') { @@ -120,7 +129,6 @@ const createHighlightPlugin = (renderer, tab)=>{ const to = line.from + tok.to; const attrs = {}; - console.log(tok); // attach URL only for links if(tok.type === 'Image' && tok.url) { diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 39103fdac..ad562e8f1 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -204,7 +204,6 @@ export function tokenizeCustomMarkdown(text) { let match; while ((match = injectionRegex.exec(lineText)) !== null) { - console.log(match.indices[1][0]); tokens.push({ line : lineNumber, from : match.indices[1][0], From 70d9614fb18325718b302e3008a603e5d0f4cad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:39:26 +0200 Subject: [PATCH 23/52] lint --- client/components/codeEditor/codeEditor.jsx | 32 ++++++--------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 90ab1fde0..c70e1109b 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -17,13 +17,13 @@ import { crosshairCursor, } from '@codemirror/view'; import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state'; -import { - foldAll as foldAllCmd, +import { + foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, - foldGutter, - foldKeymap, - foldEffect, - foldState, + foldGutter, + foldKeymap, + foldEffect, + foldState, syntaxHighlighting, syntaxTree, ensureSyntaxTree @@ -152,30 +152,14 @@ const createHighlightPlugin = (renderer, tab)=>{ class : `cm-${tok.type}` }).range(line.from) ); - if(tok.type === 'pageLine' && tab === 'brewText') { pageCount++; if(line.from === 0) pageCount--; - - decos.push( - Decoration.line({ - attributes : { - 'data-page-number' : pageCount - } - }).range(line.from) - ); + decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from)); } - if(tok.type === 'snippetLine' && tab === 'brewSnippets') { snippetCount++; - - decos.push( - Decoration.line({ - attributes : { - 'data-page-number' : snippetCount - } - }).range(line.from) - ); + decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); } } }); From aac384390b74428e1b9f0ebf81ad7fe30baa1cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:41:11 +0200 Subject: [PATCH 24/52] lint --- client/components/codeEditor/codeEditor.jsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index c70e1109b..ae91361cb 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -123,13 +123,11 @@ const createHighlightPlugin = (renderer, tab)=>{ tokens.forEach((tok)=>{ const line = view.state.doc.line(tok.line + 1); - // INLINE TOKENS (marks) if(tok.from != null && tok.to != null && tok.from < tok.to) { const from = line.from + tok.from; const to = line.from + tok.to; const attrs = {}; - // attach URL only for links if(tok.type === 'Image' && tok.url) { attrs['data-url'] = tok.url; @@ -143,10 +141,7 @@ const createHighlightPlugin = (renderer, tab)=>{ : {}) }).range(from, to) ); - } - - // LINE TOKENS - else { + } else { decos.push( Decoration.line({ class : `cm-${tok.type}` @@ -168,9 +163,7 @@ const createHighlightPlugin = (renderer, tab)=>{ return Decoration.set(decos); } }, - { - decorations : (v)=>v.decorations - } + { decorations: (v)=>v.decorations } ); }; From b39502d806a2caf337196806575558fc57256aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:42:02 +0200 Subject: [PATCH 25/52] lint more --- client/components/codeEditor/codeEditor.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index ae91361cb..ba60bf125 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -119,7 +119,6 @@ const createHighlightPlugin = (renderer, tab)=>{ } }); - tokens.forEach((tok)=>{ const line = view.state.doc.line(tok.line + 1); @@ -450,7 +449,6 @@ const CodeEditor = forwardRef( injectText : (text)=>{ const view = viewRef.current; - view.dispatch( view.state.replaceSelection(text) ); From 2971fe0679c482a789c01e771a543bdab68b3797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 11:47:00 +0200 Subject: [PATCH 26/52] add transition delay --- client/components/codeEditor/codeEditor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index da31e4c4d..16d99bc30 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -174,7 +174,7 @@ z-index:1000; pointer-events: none; opacity:0; - transition:0.2s opacity; + transition:0.2s opacity 0.5s; } &::before{ From 0f7aa0ad49aed5b2cd50110a2196d26b23868707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 11:49:41 +0200 Subject: [PATCH 27/52] change tok for token --- client/components/codeEditor/codeEditor.jsx | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index ba60bf125..254d2977d 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -119,22 +119,22 @@ const createHighlightPlugin = (renderer, tab)=>{ } }); - tokens.forEach((tok)=>{ - const line = view.state.doc.line(tok.line + 1); + tokens.forEach((token)=>{ + const line = view.state.doc.line(token.line + 1); - if(tok.from != null && tok.to != null && tok.from < tok.to) { - const from = line.from + tok.from; - const to = line.from + tok.to; + if(token.from != null && token.to != null && token.from < token.to) { + const from = line.from + token.from; + const to = line.from + token.to; const attrs = {}; - if(tok.type === 'Image' && tok.url) { + if(token.type === 'Image' && token.url) { - attrs['data-url'] = tok.url; + attrs['data-url'] = token.url; } decos.push( Decoration.mark({ - class : `cm-${tok.type}`, + class : `cm-${token.type}`, ...(Object.keys(attrs).length ? { attributes: attrs } : {}) @@ -143,15 +143,15 @@ const createHighlightPlugin = (renderer, tab)=>{ } else { decos.push( Decoration.line({ - class : `cm-${tok.type}` + class : `cm-${token.type}` }).range(line.from) ); - if(tok.type === 'pageLine' && tab === 'brewText') { + if(token.type === 'pageLine' && tab === 'brewText') { pageCount++; if(line.from === 0) pageCount--; decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from)); } - if(tok.type === 'snippetLine' && tab === 'brewSnippets') { + if(token.type === 'snippetLine' && tab === 'brewSnippets') { snippetCount++; decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); } From df4c59cbd6b6a605eb1dcee77269d571e81ee98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:06:21 +0200 Subject: [PATCH 28/52] move highlight plugin into its own file --- client/components/codeEditor/codeEditor.jsx | 130 +---------------- .../codeEditor/extensions/customHighlight.js | 132 +++++++++++++++++- 2 files changed, 130 insertions(+), 132 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 254d2977d..c5cf669bd 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 400 }] */ +/* eslint max-lines: ["error", { "max": 405 }] */ import './codeEditor.less'; import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; @@ -10,7 +10,6 @@ import { highlightActiveLine, scrollPastEnd, Decoration, - ViewPlugin, drawSelection, dropCursor, rectangularSelection, @@ -18,15 +17,12 @@ import { } from '@codemirror/view'; import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state'; import { - foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting, - syntaxTree, - ensureSyntaxTree } from '@codemirror/language'; import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands'; import { languages } from '@codemirror/language-data'; @@ -49,123 +45,11 @@ const highlightCompartment = new Compartment(); import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; -import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; -import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; +import { customHighlightPlugin, customHighlightStyle } from './extensions/customHighlight.js'; +import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; -function getUrl(node, doc) { - let url = null; - - const cursor = node.node.cursor(); - - if(cursor.firstChild()) { - do { - if(cursor.name === 'URL') { - url = doc.sliceString(cursor.from, cursor.to); - break; - } - } while (cursor.nextSibling()); - } - - return url; -} -const createHighlightPlugin = (renderer, tab)=>{ - //this function takes the custom tokens created in the tokenize function in customhighlight files - //takes the tokens defined by that function and assigns classes to them - //it also creates page number and snippet number widgets - - let tokenize; - - if(tab === 'brewStyles') { - tokenize = tokenizeCustomCSS; - } else { - tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown; - } - - return ViewPlugin.fromClass( - class { - constructor(view) { - this.decorations = this.buildDecorations(view); - } - update(update) { - if(update.docChanged) { - this.decorations = this.buildDecorations(update.view); - } - } - buildDecorations(view) { - const decos = []; - const tokens = tokenize(view.state.doc.toString()); - let pageCount = 1; - let snippetCount = 0; - - const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state); - tree.iterate({ - enter : (node)=>{ - if(node.name === 'Image') { - const url = getUrl(node, view.state.doc); - - if(!url) return; - - decos.push( - Decoration.mark({ - class : 'cm-image', - attributes : { - 'style' : `--preview-img:url(${url});` - } - }).range(node.from, node.to) - ); - } - } - }); - - tokens.forEach((token)=>{ - const line = view.state.doc.line(token.line + 1); - - if(token.from != null && token.to != null && token.from < token.to) { - const from = line.from + token.from; - const to = line.from + token.to; - - const attrs = {}; - if(token.type === 'Image' && token.url) { - - attrs['data-url'] = token.url; - } - - decos.push( - Decoration.mark({ - class : `cm-${token.type}`, - ...(Object.keys(attrs).length - ? { attributes: attrs } - : {}) - }).range(from, to) - ); - } else { - decos.push( - Decoration.line({ - class : `cm-${token.type}` - }).range(line.from) - ); - if(token.type === 'pageLine' && tab === 'brewText') { - pageCount++; - if(line.from === 0) pageCount--; - decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from)); - } - if(token.type === 'snippetLine' && tab === 'brewSnippets') { - snippetCount++; - decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); - } - } - }); - - decos.sort((a, b)=>a.from - b.from || a.to - b.to); - return Decoration.set(decos); - } - }, - { decorations: (v)=>v.decorations } - ); -}; - const setProgrammaticCursorLine = StateEffect.define(); const programmaticCursorLineField = StateField.define({ @@ -272,8 +156,6 @@ const CodeEditor = forwardRef( ? syntaxHighlighting(customHighlightStyle) : syntaxHighlighting(legacyCustomHighlightStyle); - const customHighlightPlugin = createHighlightPlugin(renderer, tab); - const languageExtension = language === 'css' ? css() : [markdown({ base: markdownLanguage, codeLanguages: languages }), html({ autoCloseTags: true })]; const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : themes[editorTheme] || themes['default']; @@ -296,7 +178,7 @@ const CodeEditor = forwardRef( }), //highlights - highlightCompartment.of([customHighlightPlugin, highlightExtension]), + highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]), themeCompartment.of(themeExtension), highlightActiveLine(), highlightActiveLineGutter(), @@ -437,10 +319,8 @@ const CodeEditor = forwardRef( ? syntaxHighlighting(customHighlightStyle) : syntaxHighlighting(legacyCustomHighlightStyle); - const customHighlightPlugin = createHighlightPlugin(renderer, tab); - view.dispatch({ - effects : highlightCompartment.reconfigure([customHighlightPlugin, highlightExtension]), + effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab), highlightExtension]), }); }, [renderer, tab]); diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index ad562e8f1..afccea2d6 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -1,5 +1,15 @@ +/* eslint max-lines: ["error", { "max": 450 }] */ import { HighlightStyle } from '@codemirror/language'; import { tags } from '@lezer/highlight'; +import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight'; +import { + Decoration, + ViewPlugin, +} from '@codemirror/view'; +import { + syntaxTree, + ensureSyntaxTree +} from '@codemirror/language'; // Making the tokens const customTags = { @@ -23,7 +33,7 @@ const customTags = { variable : 'variable', }; -export function tokenizeCustomMarkdown(text) { +function tokenizeCustomMarkdown(text) { const tokens = []; const lines = text.split('\n'); @@ -86,8 +96,8 @@ export function tokenizeCustomMarkdown(text) { if(/\~/.test(lineText)) { const strikethroughRegex = /~(?!\s)(.+?)(?{ + if(node.name === 'Image') { + const url = getUrl(node, view.state.doc); + + if(!url) return; + + decos.push( + Decoration.mark({ + class : 'cm-image', + attributes : { + 'style' : `--preview-img:url(${url});` + } + }).range(node.from, node.to) + ); + } + } + }); + + tokens.forEach((token)=>{ + const line = view.state.doc.line(token.line + 1); + + if(token.from != null && token.to != null && token.from < token.to) { + const from = line.from + token.from; + const to = line.from + token.to; + + const attrs = {}; + if(token.type === 'Image' && token.url) { + + attrs['data-url'] = token.url; + } + + decos.push( + Decoration.mark({ + class : `cm-${token.type}`, + ...(Object.keys(attrs).length + ? { attributes: attrs } + : {}) + }).range(from, to) + ); + } else { + decos.push( + Decoration.line({ + class : `cm-${token.type}` + }).range(line.from) + ); + if(token.type === 'pageLine' && tab === 'brewText') { + pageCount++; + if(line.from === 0) pageCount--; + decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from)); + } + if(token.type === 'snippetLine' && tab === 'brewSnippets') { + snippetCount++; + decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); + } + } + }); + + decos.sort((a, b)=>a.from - b.from || a.to - b.to); + return Decoration.set(decos); + } + }, + { decorations: (v)=>v.decorations } + ); +}; \ No newline at end of file From c66310fbe7401cc1de7e0a0fcb2d4b8074830c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:09:43 +0200 Subject: [PATCH 29/52] linting --- client/components/codeEditor/extensions/customHighlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index afccea2d6..63251e49e 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -316,7 +316,6 @@ export const customHighlightStyle = HighlightStyle.define([ ]); - function getUrl(node, doc) { let url = null; @@ -333,6 +332,7 @@ function getUrl(node, doc) { return url; } + export function customHighlightPlugin(renderer, tab) { //this function takes the custom tokens created in the tokenize function in customhighlight files //takes the tokens defined by that function and assigns classes to them From 097b941580dad433f1c0c181b3087c7072aa7574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:45:46 +0200 Subject: [PATCH 30/52] move to use html img element --- client/components/codeEditor/codeEditor.less | 40 ++++++------------ .../codeEditor/extensions/customHighlight.js | 40 ++++++++++++++++-- client/icons/Broken-image-icon-in-Chrome.png | Bin 0 -> 9227 bytes 3 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 client/icons/Broken-image-icon-in-Chrome.png diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 16d99bc30..fbb2cacaa 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -161,44 +161,28 @@ outline : 1px inset #00000055 !important; } - .cm-image[style] { - position: relative; - - &::before, &::after { - content:""; + .cm-image { + position:relative; + + .cm-preview { + object-fit: contain; position:absolute; bottom:0; left:0; - translate:0 100%; - display:block; - z-index:1000; - pointer-events: none; - opacity:0; - transition:0.2s opacity 0.5s; - } - - &::before{ + height:200px; width:200px; height:200px; background-color: #fff; border-radius:10px; border:3px solid grey; + pointer-events: none; + opacity:0; + transition:0.2s opacity 0.5s; + translate:0 100%; + z-index:1000; } - &::after { - width:190px; - height:190px; - //padding of 5px + 3px border: 8px - translate:8px calc(100% + 8px); - background-image: var(--preview-img); - background-size:contain; - background-repeat:no-repeat; - background-position:center; - filter:drop-shadow(0 0 5px #0008); - } - - &:hover::before, - &:hover::after { + &:hover .cm-preview { opacity:1; } } diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 63251e49e..2a63bb398 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -333,6 +333,33 @@ function getUrl(node, doc) { return url; } +import { WidgetType } from '@codemirror/view'; + +class ImageWidget extends WidgetType { + constructor(url) { + super(); + this.url = url; + } + + toDOM() { + const img = document.createElement('img'); + img.loading = "lazy"; + img.className = 'cm-preview'; + img.src = this.url; + + + img.onerror = ()=>{ + img.src = 'client/icons/Broken-image-icon-in-Chrome.png'; + }; + + return img; + } + + eq(other) { + return other.url === this.url; + } +} + export function customHighlightPlugin(renderer, tab) { //this function takes the custom tokens created in the tokenize function in customhighlight files //takes the tokens defined by that function and assigns classes to them @@ -368,16 +395,21 @@ export function customHighlightPlugin(renderer, tab) { if(node.name === 'Image') { const url = getUrl(node, view.state.doc); + console.log(node.node.lastChild.from); + if(!url) return; decos.push( Decoration.mark({ - class : 'cm-image', - attributes : { - 'style' : `--preview-img:url(${url});` - } + class : 'cm-image' }).range(node.from, node.to) ); + decos.push( + Decoration.widget({ + widget : new ImageWidget(url), + side : 1 + }).range(node.node.lastChild.from) + ); } } }); diff --git a/client/icons/Broken-image-icon-in-Chrome.png b/client/icons/Broken-image-icon-in-Chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..6c4d4fd4058dafcff3b47f056aca571b739b6163 GIT binary patch literal 9227 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 F@*masY7PJZ literal 0 HcmV?d00001 From 3c4bafd1ad7ab139e3d962445faad5fa93875d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:46:10 +0200 Subject: [PATCH 31/52] eslint changes --- client/components/codeEditor/extensions/customHighlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 2a63bb398..79a4a18c9 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 450 }] */ +/* eslint max-lines: ["error", { "max": 500 }] */ import { HighlightStyle } from '@codemirror/language'; import { tags } from '@lezer/highlight'; import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight'; From d630882f18f03c21fd4be0ba20c7a125c963a168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:51:45 +0200 Subject: [PATCH 32/52] add padding --- client/components/codeEditor/codeEditor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index fbb2cacaa..f3658f290 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -172,6 +172,7 @@ height:200px; width:200px; height:200px; + padding:5px; background-color: #fff; border-radius:10px; border:3px solid grey; From 0bee33efc0ff85c3e231a67a6b158c13dd5dc5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 20:58:40 +0200 Subject: [PATCH 33/52] file removal --- client/icons/Broken-image-icon-in-Chrome.png | Bin 9227 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 client/icons/Broken-image-icon-in-Chrome.png diff --git a/client/icons/Broken-image-icon-in-Chrome.png b/client/icons/Broken-image-icon-in-Chrome.png deleted file mode 100644 index 6c4d4fd4058dafcff3b47f056aca571b739b6163..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9227 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 F@*masY7PJZ From 16948f451fe86ec6dd4d266c204b9538d80f2010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 14 May 2026 11:08:09 +0200 Subject: [PATCH 34/52] add a comment --- client/components/codeEditor/extensions/customHighlight.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 79a4a18c9..410bfe0d1 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -395,7 +395,10 @@ export function customHighlightPlugin(renderer, tab) { if(node.name === 'Image') { const url = getUrl(node, view.state.doc); - console.log(node.node.lastChild.from); + const widgetPosition = node.node.lastChild.from; + //this is not exactly standard, but should hold, + //and is the shortest way i could find of positioning + //the image inside the cm-image node if(!url) return; @@ -408,7 +411,7 @@ export function customHighlightPlugin(renderer, tab) { Decoration.widget({ widget : new ImageWidget(url), side : 1 - }).range(node.node.lastChild.from) + }).range(widgetPosition) ); } } From 074af5e8e0dba553ba3e30c75cb828f7acea5fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sat, 16 May 2026 16:28:53 +0200 Subject: [PATCH 35/52] more solid block tokenizing --- client/components/codeEditor/codeEditor.less | 3 ++- .../codeEditor/extensions/customHighlight.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index f3658f290..ca77f29a8 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -63,7 +63,8 @@ &.term { color : rgb(96, 117, 143); } &.definition { color : rgb(97, 57, 178); } } - .cm-block:not(.cm-comment) { + .cm-block:not(.cm-comment), + .cm-block:not(.cm-comment) * { font-weight : bold; color : purple; } diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 410bfe0d1..9f08c2e1a 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -250,7 +250,16 @@ function tokenizeCustomMarkdown(text) { /^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/, ); if(match) endCh = match.index + match[0].length; - tokens.push({ line: lineNumber, type: customTags.block }); + const closingMatch = lineText.match(/ *(}})/d); + + if(closingMatch) { + console.log('closing', closingMatch); + console.log(closingMatch.indices[1][0], closingMatch.indices[1][1]) + tokens.push({ line: lineNumber, from: closingMatch.indices[1][0], to: closingMatch.indices[1][1], type: customTags.block }); + } else { + tokens.push({ line: lineNumber, type: customTags.block }); + } + } }); From dff93002d01d1d661471bddb7855ad001c142884 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Mon, 18 May 2026 21:59:56 -0500 Subject: [PATCH 36/52] Clarify Regex behavior Add short circuit so the full hoisting only happens on brew load rather than every parse. Remove stray CR in unrelated file. --- client/components/codeEditor/codeEditor.jsx | 2 +- client/homebrew/brewRenderer/brewRenderer.jsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 3c75d52b0..3b0ff5854 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -454,4 +454,4 @@ const CodeEditor = forwardRef( }, ); -export default CodeEditor; \ No newline at end of file +export default CodeEditor; diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index b77a580c7..4925397ff 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -42,6 +42,7 @@ const BrewPage = (props)=>{ props = { contents : '', index : 0, + hoisted : false, ...props }; const pageRef = useRef(null); @@ -234,11 +235,16 @@ const BrewRenderer = (props)=>{ renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ - const forceRender = checkHoists && (page.match(/([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g)); + const varsOnPageRegex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Find out if there are any vars on the page. + const forceRender = checkHoists && + !props.hoisted && + (page.match(varsOnPageRegex)); // forceRender forces pages outside of the PPR range to render if true. + // This is necessary on the first load to fully populate the variable table. if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){ renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range } }); + if(!props.hoisted) { props.hoisted = true; } // Only fully hoist once. return renderedPages; }; From 7a0348536cb8afdd3655a798b3300c612f74ca41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sat, 23 May 2026 09:18:04 +0200 Subject: [PATCH 37/52] fix image request going crazy --- .../codeEditor/extensions/customHighlight.js | 4 +--- client/icons/broken-image.jpg | Bin 0 -> 14669 bytes 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 client/icons/broken-image.jpg diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 9f08c2e1a..33c68da7a 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -253,8 +253,6 @@ function tokenizeCustomMarkdown(text) { const closingMatch = lineText.match(/ *(}})/d); if(closingMatch) { - console.log('closing', closingMatch); - console.log(closingMatch.indices[1][0], closingMatch.indices[1][1]) tokens.push({ line: lineNumber, from: closingMatch.indices[1][0], to: closingMatch.indices[1][1], type: customTags.block }); } else { tokens.push({ line: lineNumber, type: customTags.block }); @@ -358,7 +356,7 @@ class ImageWidget extends WidgetType { img.onerror = ()=>{ - img.src = 'client/icons/Broken-image-icon-in-Chrome.png'; + img.src = 'client/icons/broken-image.jpg'; }; return img; diff --git a/client/icons/broken-image.jpg b/client/icons/broken-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..52fa33195d2f902715a918e7d9610049cf670487 GIT binary patch literal 14669 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 Date: Wed, 27 May 2026 18:42:47 +0200 Subject: [PATCH 38/52] brewCleanup to functional component --- .../brewUtils/brewCleanup/brewCleanup.jsx | 92 ++++++++----------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index 6cec01178..53498a54b 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -1,71 +1,59 @@ -import React from 'react'; -import createReactClass from 'create-react-class'; +import React, { useState } from 'react'; import request from 'superagent'; -const BrewCleanup = createReactClass({ - displayName : 'BrewCleanup', - getDefaultProps(){ - return {}; - }, - getInitialState() { - return { - count : 0, +const BrewCleanup = ({})=>{ + const [count, setCount] = useState(0); + const [pending, setPending] = useState(false); + const [primed, setPrimed] = useState(false); + const [error, setError] = useState(null); - pending : false, - primed : false, - err : null - }; - }, - prime(){ - this.setState({ pending: true }); + const prime = ()=>{ + setPending(true); request.get('/admin/cleanup') - .then((res)=>this.setState({ count: res.body.count, primed: true })) - .catch((err)=>this.setState({ error: err })) - .finally(()=>this.setState({ pending: false })); - }, - cleanup(){ - this.setState({ pending: true }); + .then((res)=>{setCount(res.body.count);setPrimed(true);}) + .catch((err)=>setError(err)) + .finally(()=>setPending(false)); + }; + const cleanup = ()=>{ + setPending(true); request.post('/admin/cleanup') - .then((res)=>this.setState({ count: res.body.count })) - .catch((err)=>this.setState({ error: err })) - .finally(()=>this.setState({ pending: false, primed: false })); - }, - renderPrimed(){ - if(!this.state.primed) return; + .then((res)=>setCount(res.body.count)) + .catch((err)=>setError(err)) + .finally(()=>{setPending(false);setPrimed(false);}); + }; + const renderPrimed = ()=>{ + if(!primed) return; + + if(!count) return
No Matching Brews found.
; - if(!this.state.count){ - return
No Matching Brews found.
; - } return
- - Found {this.state.count} Brews that could be removed. + Found {count} Brews that could be removed.
; - }, - render(){ - return
-

Brew Cleanup

-

Removes very short brews to tidy up the database

+ }; - - {this.renderPrimed()} + return
+

Brew Cleanup

+

Removes very short brews to tidy up the database

- {this.state.error - &&
{this.state.error.toString()}
+
; - } -}); + + {renderPrimed()} + + {error &&
{error.toString()}
} +
; + +}; export default BrewCleanup; From fff0b4d670dccfdadc3f75c5c4227f351b2e5e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 27 May 2026 18:49:48 +0200 Subject: [PATCH 39/52] moving to try catch better --- .../brewUtils/brewCleanup/brewCleanup.jsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index 53498a54b..721ec1e00 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -7,21 +7,34 @@ const BrewCleanup = ({})=>{ const [primed, setPrimed] = useState(false); const [error, setError] = useState(null); - const prime = ()=>{ + const prime = async ()=>{ setPending(true); - request.get('/admin/cleanup') - .then((res)=>{setCount(res.body.count);setPrimed(true);}) - .catch((err)=>setError(err)) - .finally(()=>setPending(false)); + try { + const res = await request.get('/admin/cleanup'); + + setCount(res.body.count); + setPrimed(true); + } catch (err) { + setError(err); + } finally { + setPending(false); + } }; - const cleanup = ()=>{ + + const cleanup = async ()=>{ setPending(true); - request.post('/admin/cleanup') - .then((res)=>setCount(res.body.count)) - .catch((err)=>setError(err)) - .finally(()=>{setPending(false);setPrimed(false);}); + try { + const res = await request.post('/admin/cleanup'); + + setCount(res.body.count); + } catch (err) { + setError(err); + } finally { + setPending(false); + setPrimed(false); + } }; const renderPrimed = ()=>{ if(!primed) return; From e7680ef80bf02ab541f5f6ebc38c7e3bca6f150c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 27 May 2026 22:43:57 +0200 Subject: [PATCH 40/52] proper cleaning function --- .../brewUtils/brewCleanup/brewCleanup.jsx | 41 +++++++++++++++++++ server/admin.api.js | 17 ++++---- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index 721ec1e00..85d77ba85 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -1,8 +1,10 @@ import React, { useState } from 'react'; import request from 'superagent'; +import Moment from 'moment'; const BrewCleanup = ({})=>{ const [count, setCount] = useState(0); + const [brewCollection, setBrewCollection] = useState([]); const [pending, setPending] = useState(false); const [primed, setPrimed] = useState(false); const [error, setError] = useState(null); @@ -14,6 +16,7 @@ const BrewCleanup = ({})=>{ const res = await request.get('/admin/cleanup'); setCount(res.body.count); + setBrewCollection(res.body.brewCollection); setPrimed(true); } catch (err) { setError(err); @@ -36,6 +39,43 @@ const BrewCleanup = ({})=>{ setPrimed(false); } }; + const renderBrewList = ()=>{ + if(!brewCollection) { + console.log(brewCollection) + return null; + } + + return <> +

{`Results - ${brewCollection.length} brews` }

+ + + + + + + + + + + + {brewCollection + .sort((a, b)=>{ // Sort brews from most recently updated + if(a.updatedAt > b.updatedAt) return -1; + return 1; + }) + .map((brew, idx)=>{ + return + + + + + + ; + })} + +
TitleShareLast UpdateCreatedStorage
{brew.title || 'No Title'}{brew.shareId}{Moment(brew.updatedAt).fromNow()}{brew.createdAt ? Moment(brew.createdAt).fromNow() : 'No creation date'}{brew.googleId ? 'Google' : 'Homebrewery'}
+ ; + }; const renderPrimed = ()=>{ if(!primed) return; @@ -49,6 +89,7 @@ const BrewCleanup = ({})=>{ } Found {count} Brews that could be removed. + {renderBrewList()} ; }; diff --git a/server/admin.api.js b/server/admin.api.js index f55eefdf2..60582b75f 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -41,12 +41,13 @@ export default function createAdminApi(vite) { 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 } + updatedAt : { $lt: Moment().subtract(365, 'days').toDate() }, + lastViewed : { $lt: Moment().subtract(365, 'days').toDate() } + }}, + { $match: { + authors : [], + }}, + { $limit: 300 } ]; /* Search for brews that aren't compressed (missing the compressed text field) */ @@ -54,10 +55,10 @@ export default function createAdminApi(vite) { '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 + // Search for up to 100 brews that have not been viewed or updated in a year router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) - .then((objs)=>res.json({ count: objs.length })) + .then((objs)=>res.json({ count: objs.length, brewCollection : objs })) .catch((error)=>{ console.error(error); res.status(500).json({ error: 'Internal Server Error' }); From 3b71f6fa0ba3966f6b205bb861b6c646ee3a91ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 28 May 2026 10:36:24 +0200 Subject: [PATCH 41/52] change prime to found --- .../brewUtils/brewCleanup/brewCleanup.jsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index 85d77ba85..be1ca0a51 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -6,10 +6,10 @@ const BrewCleanup = ({})=>{ const [count, setCount] = useState(0); const [brewCollection, setBrewCollection] = useState([]); const [pending, setPending] = useState(false); - const [primed, setPrimed] = useState(false); + const [found, setFound] = useState(false); const [error, setError] = useState(null); - const prime = async ()=>{ + const find = async ()=>{ setPending(true); try { @@ -17,7 +17,7 @@ const BrewCleanup = ({})=>{ setCount(res.body.count); setBrewCollection(res.body.brewCollection); - setPrimed(true); + setFound(true); } catch (err) { setError(err); } finally { @@ -36,7 +36,7 @@ const BrewCleanup = ({})=>{ setError(err); } finally { setPending(false); - setPrimed(false); + setFound(false); } }; const renderBrewList = ()=>{ @@ -51,7 +51,6 @@ const BrewCleanup = ({})=>{ Title - Share Last Update Created Storage @@ -66,7 +65,8 @@ const BrewCleanup = ({})=>{ .map((brew, idx)=>{ return {brew.title || 'No Title'} - {brew.shareId} + {//{brew.shareId} + } {Moment(brew.updatedAt).fromNow()} {brew.createdAt ? Moment(brew.createdAt).fromNow() : 'No creation date'} {brew.googleId ? 'Google' : 'Homebrewery'} @@ -76,8 +76,8 @@ const BrewCleanup = ({})=>{ ; }; - const renderPrimed = ()=>{ - if(!primed) return; + const renderFound = ()=>{ + if(!found) return; if(!count) return
No Matching Brews found.
; @@ -97,13 +97,13 @@ const BrewCleanup = ({})=>{

Brew Cleanup

Removes very short brews to tidy up the database

- - {renderPrimed()} + {renderFound()} {error &&
{error.toString()}
} ; From fea86dc261caaa78f5d798f149d3a33c44f74723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 28 May 2026 10:36:35 +0200 Subject: [PATCH 42/52] add secondary pipeline --- server/admin.api.js | 46 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/server/admin.api.js b/server/admin.api.js index 60582b75f..e16024326 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -39,7 +39,17 @@ export default function createAdminApi(vite) { } }; - const junkBrewPipeline = [ + const junkBrewsPipeline = [ + { $match : { + updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, + lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } + } }, + { $project: { textBinSize: { $binarySize: '$textBin' } } }, + { $match: { textBinSize: { $lt: 140 } } }, + { $limit: 300 } + ]; + + const lostBrewsPipeline = [ { $match : { updatedAt : { $lt: Moment().subtract(365, 'days').toDate() }, lastViewed : { $lt: Moment().subtract(365, 'days').toDate() } @@ -55,9 +65,9 @@ export default function createAdminApi(vite) { 'text' : { '$exists': true } }).lean().limit(10000).select('_id'); - // Search for up to 100 brews that have not been viewed or updated in a year - router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{ - HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) + // Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes + router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{ + HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 }) .then((objs)=>res.json({ count: objs.length, brewCollection : objs })) .catch((error)=>{ console.error(error); @@ -65,8 +75,8 @@ export default function createAdminApi(vite) { }); }); - // 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)=>{ + // Delete up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes + router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) .then((docs)=>{ const ids = docs.map((doc)=>doc._id); @@ -79,6 +89,30 @@ export default function createAdminApi(vite) { }); }); + // Search for up to 300 unauthored brews that have not been viewed or updated in a year + router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{ + HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 }) + .then((objs)=>res.json({ count: objs.length, brewCollection : objs })) + .catch((error)=>{ + console.error(error); + res.status(500).json({ error: 'Internal Server Error' }); + }); + }); + + // Delete up to 300 unauthored brews that have not been viewed or updated in a year + router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{ + HomebrewModel.aggregate(lostBrewPipeline).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, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{ return res.json(req.brew); From 1838a57e53ce15bc3836c91d5ee4f3e74185a5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 29 May 2026 11:32:09 +0200 Subject: [PATCH 43/52] final version --- .../brewUtils/brewCleanup/brewCleanup.jsx | 120 ++++++++++++------ server/admin.api.js | 8 +- 2 files changed, 87 insertions(+), 41 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index be1ca0a51..cd60e6728 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -3,21 +3,29 @@ import request from 'superagent'; import Moment from 'moment'; const BrewCleanup = ({})=>{ - const [count, setCount] = useState(0); - const [brewCollection, setBrewCollection] = useState([]); + const [junkBrewCollection, setJunkBrewCollection] = useState([]); + const [lostBrewCollection, setLostBrewCollection] = useState([]); const [pending, setPending] = useState(false); - const [found, setFound] = useState(false); const [error, setError] = useState(null); - const find = async ()=>{ + const find = async (type)=>{ setPending(true); - try { - const res = await request.get('/admin/cleanup'); + if(type === 'junk') try { + const res = await request.get('/admin/cleanupJunk'); - setCount(res.body.count); - setBrewCollection(res.body.brewCollection); - setFound(true); + setJunkBrewCollection(res.body.brewCollection); + } catch (err) { + setError(err); + } finally { + setPending(false); + } + + if(type === 'lost') try { + + const res = await request.get('/admin/cleanupLost'); + + setLostBrewCollection(res.body.brewCollection); } catch (err) { setError(err); } finally { @@ -25,28 +33,42 @@ const BrewCleanup = ({})=>{ } }; - const cleanup = async ()=>{ + const cleanup = async (type)=>{ setPending(true); - try { - const res = await request.post('/admin/cleanup'); + if(type === 'junk') try { + console.log('deleting junk') + const res = await request.post('/admin/cleanupJunk'); - setCount(res.body.count); } catch (err) { setError(err); } finally { setPending(false); - setFound(false); + setJunkBrewCollection([]); + } + + if(type === 'lost') try { + const res = await request.post('/admin/cleanupLost'); + + } catch (err) { + setError(err); + } finally { + setPending(false); + setLostBrewCollection([]); } }; - const renderBrewList = ()=>{ - if(!brewCollection) { - console.log(brewCollection) + + const renderBrewList = (type)=>{ + + const brewList = type === 'lost' ? lostBrewCollection : junkBrewCollection; + + if(!brewList || brewList.length === 0) { + console.log(brewList); return null; } - + return <> -

{`Results - ${brewCollection.length} brews` }

+

{`Results - ${brewList.length} brews` }

@@ -57,7 +79,7 @@ const BrewCleanup = ({})=>{ - {brewCollection + {brewList .sort((a, b)=>{ // Sort brews from most recently updated if(a.updatedAt > b.updatedAt) return -1; return 1; @@ -65,8 +87,6 @@ const BrewCleanup = ({})=>{ .map((brew, idx)=>{ return - {// - } @@ -76,36 +96,60 @@ const BrewCleanup = ({})=>{
{brew.title || 'No Title'}{brew.shareId}{Moment(brew.updatedAt).fromNow()} {brew.createdAt ? Moment(brew.createdAt).fromNow() : 'No creation date'} {brew.googleId ? 'Google' : 'Homebrewery'}
; }; - const renderFound = ()=>{ - if(!found) return; + const renderFound = (type)=>{ - if(!count) return
No Matching Brews found.
; + if(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0) return
No Matching Brews found.
; return
- - Found {count} Brews that could be removed. - {renderBrewList()} + Found {type === 'junk' ? junkBrewCollection.length : lostBrewCollection.length} Brews that could be removed. + +
; + }; + const renderJunkBrewCleanup = ()=>{ + return
+

Junk brews

+

Removes very short brews to tidy up the database

+ + + {renderFound('junk')} + + {error &&
{error.toString()}
} +
; + }; + const renderLostBrewCleanup = ()=>{ + return
+

Lost brews

+

Removes very short brews to tidy up the database

+ + + {renderFound('lost')} + + {error &&
{error.toString()}
}
; }; return

Brew Cleanup

-

Removes very short brews to tidy up the database

+ {renderJunkBrewCleanup()} + {renderLostBrewCleanup()} - - {renderFound()} - - {error &&
{error.toString()}
}
; }; diff --git a/server/admin.api.js b/server/admin.api.js index e16024326..4033c30c6 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -44,7 +44,7 @@ export default function createAdminApi(vite) { updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } } }, - { $project: { textBinSize: { $binarySize: '$textBin' } } }, + { $project: { _id: 1, textBinSize: { $binarySize: '$textBin' } } }, { $match: { textBinSize: { $lt: 140 } } }, { $limit: 300 } ]; @@ -77,9 +77,11 @@ export default function createAdminApi(vite) { // Delete up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{ - HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) + console.log('deleting'); + HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 }) .then((docs)=>{ const ids = docs.map((doc)=>doc._id); + console.log(ids); return HomebrewModel.deleteMany({ _id: { $in: ids } }); }).then((result)=>{ res.json({ count: result.deletedCount }); @@ -101,7 +103,7 @@ export default function createAdminApi(vite) { // Delete up to 300 unauthored brews that have not been viewed or updated in a year router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{ - HomebrewModel.aggregate(lostBrewPipeline).option({ maxTimeMS: 60000 }) + HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 }) .then((docs)=>{ const ids = docs.map((doc)=>doc._id); return HomebrewModel.deleteMany({ _id: { $in: ids } }); From 55c4504ed97669a427aea3fbb0e400ddfd009b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 29 May 2026 11:32:31 +0200 Subject: [PATCH 44/52] remove logs --- server/admin.api.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/admin.api.js b/server/admin.api.js index 4033c30c6..c47e72c60 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -77,11 +77,9 @@ export default function createAdminApi(vite) { // Delete up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{ - console.log('deleting'); HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 }) .then((docs)=>{ const ids = docs.map((doc)=>doc._id); - console.log(ids); return HomebrewModel.deleteMany({ _id: { $in: ids } }); }).then((result)=>{ res.json({ count: result.deletedCount }); From ed6bf9f1bb983be303d371bdfdc2dfbe1bf24d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sat, 30 May 2026 00:08:07 +0200 Subject: [PATCH 45/52] last changes --- client/admin/admin.less | 4 ++++ .../brewUtils/brewCleanup/brewCleanup.jsx | 11 ++++++----- server/admin.api.js | 18 ++++++++++-------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/client/admin/admin.less b/client/admin/admin.less index 432f92e8b..e66ead5e3 100644 --- a/client/admin/admin.less +++ b/client/admin/admin.less @@ -111,6 +111,10 @@ body { vertical-align : middle; text-align : center; border-right : 1px solid; + max-width:50ch; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; &:last-child { border-right : none; } } diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index cd60e6728..5f19fb474 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -67,6 +67,7 @@ const BrewCleanup = ({})=>{ return null; } + return <>

{`Results - ${brewList.length} brews` }

@@ -74,21 +75,21 @@ const BrewCleanup = ({})=>{ - + {brewList .sort((a, b)=>{ // Sort brews from most recently updated - if(a.updatedAt > b.updatedAt) return -1; + if(a.lastViewed > b.lastViewed) return -1; return 1; }) .map((brew, idx)=>{ return - + ; })} @@ -101,7 +102,7 @@ const BrewCleanup = ({})=>{ if(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0) return
No Matching Brews found.
; return
- {renderBrewList(type)} + Found {type === 'junk' ? junkBrewCollection.length : lostBrewCollection.length} Brews that could be removed. - + {renderBrewList(type)}
; }; const renderJunkBrewCleanup = ()=>{ diff --git a/server/admin.api.js b/server/admin.api.js index c47e72c60..161570dc2 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -50,14 +50,16 @@ export default function createAdminApi(vite) { ]; const lostBrewsPipeline = [ - { $match : { - updatedAt : { $lt: Moment().subtract(365, 'days').toDate() }, - lastViewed : { $lt: Moment().subtract(365, 'days').toDate() } - }}, - { $match: { - authors : [], - }}, - { $limit: 300 } + { + $match: { + authors: [], + updatedAt: { $lt: Moment().subtract(9, 'years').toDate() }, + lastViewed: { $lt: Moment().subtract(9, 'years').toDate() } + } + }, + { + $limit: 1000 + } ]; /* Search for brews that aren't compressed (missing the compressed text field) */ From b65105598a3e7b8db7d947df4f88f688d61ceda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Tue, 2 Jun 2026 14:34:18 +0200 Subject: [PATCH 46/52] going for 6 years for now --- client/admin/brewUtils/brewCleanup/brewCleanup.jsx | 2 +- server/admin.api.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index 5f19fb474..e2acb2a9d 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -89,7 +89,7 @@ const BrewCleanup = ({})=>{ return - + ; })} diff --git a/server/admin.api.js b/server/admin.api.js index 161570dc2..451e2aa83 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -53,12 +53,12 @@ export default function createAdminApi(vite) { { $match: { authors: [], - updatedAt: { $lt: Moment().subtract(9, 'years').toDate() }, - lastViewed: { $lt: Moment().subtract(9, 'years').toDate() } + updatedAt: { $lt: Moment().subtract(6, 'years').toDate() }, + lastViewed: { $lt: Moment().subtract(6, 'years').toDate() } } }, { - $limit: 1000 + $limit: 500 } ]; From 0c0579a9d3e9fc0e76591f05a9199b474835c261 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:33:00 +0000 Subject: [PATCH 47/52] Bump the prod-dependencies group across 1 directory with 12 updates Bumps the prod-dependencies group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) | `7.29.0` | `7.29.7` | | [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) | `7.29.0` | `7.29.7` | | [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) | `7.28.5` | `7.29.7` | | [@googleapis/drive](https://github.com/googleapis/google-api-nodejs-client) | `20.1.0` | `20.2.0` | | [dedent](https://github.com/dmnd/dedent) | `1.7.1` | `1.7.2` | | [fflate](https://github.com/101arrowz/fflate) | `0.8.2` | `0.8.3` | | [idb-keyval](https://github.com/jakearchibald/idb-keyval) | `6.2.2` | `6.2.5` | | [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.1` | `4.2.0` | | [marked-emoji](https://github.com/UziTech/marked-emoji) | `2.0.2` | `2.0.3` | | [marked-gfm-heading-id](https://github.com/markedjs/marked-gfm-heading-id) | `4.1.3` | `4.1.4` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` | Updates `@babel/core` from 7.29.0 to 7.29.7 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-core) Updates `@babel/plugin-transform-runtime` from 7.29.0 to 7.29.7 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-plugin-transform-runtime) Updates `@babel/preset-react` from 7.28.5 to 7.29.7 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-preset-react) Updates `@googleapis/drive` from 20.1.0 to 20.2.0 - [Release notes](https://github.com/googleapis/google-api-nodejs-client/releases) - [Changelog](https://github.com/googleapis/google-api-nodejs-client/blob/20.2.0/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-nodejs-client/compare/20.1.0...20.2.0) Updates `dedent` from 1.7.1 to 1.7.2 - [Release notes](https://github.com/dmnd/dedent/releases) - [Changelog](https://github.com/dmnd/dedent/blob/main/CHANGELOG.md) - [Commits](https://github.com/dmnd/dedent/compare/v1.7.1...v1.7.2) Updates `fflate` from 0.8.2 to 0.8.3 - [Release notes](https://github.com/101arrowz/fflate/releases) - [Changelog](https://github.com/101arrowz/fflate/blob/master/CHANGELOG.md) - [Commits](https://github.com/101arrowz/fflate/compare/v0.8.2...v0.8.3) Updates `idb-keyval` from 6.2.2 to 6.2.5 - [Changelog](https://github.com/jakearchibald/idb-keyval/blob/main/CHANGELOG.md) - [Commits](https://github.com/jakearchibald/idb-keyval/compare/v6.2.2...v6.2.5) Updates `js-yaml` from 4.1.1 to 4.2.0 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/commits) Updates `marked-emoji` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/UziTech/marked-emoji/releases) - [Changelog](https://github.com/UziTech/marked-emoji/blob/main/release.config.cjs) - [Commits](https://github.com/UziTech/marked-emoji/compare/v2.0.2...v2.0.3) Updates `marked-gfm-heading-id` from 4.1.3 to 4.1.4 - [Release notes](https://github.com/markedjs/marked-gfm-heading-id/releases) - [Changelog](https://github.com/markedjs/marked-gfm-heading-id/blob/main/release.config.cjs) - [Commits](https://github.com/markedjs/marked-gfm-heading-id/compare/v4.1.3...v4.1.4) Updates `react` from 19.2.6 to 19.2.7 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react) Updates `react-dom` from 19.2.6 to 19.2.7 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.29.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: "@babel/plugin-transform-runtime" dependency-version: 7.29.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: "@babel/preset-react" dependency-version: 7.29.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-dependencies - dependency-name: "@googleapis/drive" dependency-version: 20.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-dependencies - dependency-name: dedent dependency-version: 1.7.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: fflate dependency-version: 0.8.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: idb-keyval dependency-version: 6.2.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-dependencies - dependency-name: marked-emoji dependency-version: 2.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: marked-gfm-heading-id dependency-version: 4.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: react dependency-version: 19.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: react-dom dependency-version: 19.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 222 ++++++++++++++++++++++------------------------ package.json | 24 ++--- 2 files changed, 120 insertions(+), 126 deletions(-) diff --git a/package-lock.json b/package-lock.json index eff0d26c8..da0a66e85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-runtime": "^7.29.0", + "@babel/core": "^7.29.7", + "@babel/plugin-transform-runtime": "^7.29.7", "@babel/preset-env": "^7.29.5", - "@babel/preset-react": "^7.28.5", + "@babel/preset-react": "^7.29.7", "@babel/runtime": "^7.29.2", "@codemirror/autocomplete": "^6.20.2", "@codemirror/commands": "^6.10.3", @@ -27,7 +27,7 @@ "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.43.0", "@dmsnell/diff-match-patch": "^1.1.0", - "@googleapis/drive": "^20.1.0", + "@googleapis/drive": "^20.2.0", "@lezer/highlight": "^1.2.3", "@sanity/diff-match-patch": "^3.2.0", "@vitejs/plugin-react": "^5.1.2", @@ -38,24 +38,24 @@ "core-js": "^3.49.0", "cors": "^2.8.5", "create-react-class": "^15.7.0", - "dedent": "^1.7.1", + "dedent": "^1.7.2", "express": "^5.1.0", "express-async-handler": "^1.2.0", "express-static-gzip": "3.0.1", - "fflate": "^0.8.2", + "fflate": "^0.8.3", "fs-extra": "^11.3.5", "hash-wasm": "^4.12.0", - "idb-keyval": "^6.2.2", - "js-yaml": "^4.1.1", + "idb-keyval": "^6.2.5", + "js-yaml": "^4.2.0", "jwt-simple": "^0.5.6", "less": "^4.6.4", "lodash": "^4.18.1", "marked": "15.0.12", "marked-alignment-paragraphs": "^1.0.0", "marked-definition-lists": "^1.0.1", - "marked-emoji": "^2.0.2", + "marked-emoji": "^2.0.3", "marked-extended-tables": "^2.0.1", - "marked-gfm-heading-id": "^4.1.3", + "marked-gfm-heading-id": "^4.1.4", "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.4", @@ -66,8 +66,8 @@ "nanoid": "5.1.11", "nconf": "^0.13.0", "node": "^25.9.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-frame-component": "^5.3.2", "react-router": "^7.15.1", "sanitize-filename": "1.6.4", @@ -211,20 +211,20 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -500,13 +500,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -746,12 +746,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1523,12 +1523,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1538,16 +1538,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1557,12 +1557,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" + "@babel/plugin-transform-react-jsx": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1602,13 +1602,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1664,13 +1664,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1935,17 +1935,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -3352,9 +3352,9 @@ } }, "node_modules/@googleapis/drive": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-20.1.0.tgz", - "integrity": "sha512-8/gapeLuZ3igooEGLE1AL3Zp5fjqigZQNmyjehb4QF7JVJh6FXF+72AcQGGsFypwJb2S68HIuQFomHBw4RJSOg==", + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-20.2.0.tgz", + "integrity": "sha512-6xPjqqLl34jBXWYMO34ywLVQsT8FgH948x8YI9o6Tdqs7+n4QRyvkC+BcvVQaAPNc+pyqsvZAXcbdMEcGgDUGw==", "license": "Apache-2.0", "dependencies": { "googleapis-common": "^8.0.0" @@ -6521,9 +6521,9 @@ "license": "MIT" }, "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -7590,9 +7590,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, "node_modules/file-entry-cache": { @@ -8520,9 +8520,9 @@ } }, "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.5.tgz", + "integrity": "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q==", "license": "Apache-2.0" }, "node_modules/ignore": { @@ -9908,9 +9908,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10328,12 +10338,12 @@ } }, "node_modules/marked-emoji": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.2.tgz", - "integrity": "sha512-EFnLQn4wTyf+6pXfptkm83Z2mt3VbdEYedHBAsDpwUas5n5satsj42RGqAijBpmetgGerI1EzUuzf7NIccINUQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.3.tgz", + "integrity": "sha512-fChW/AfUqCHgoEC1nFDgiw3OR/qsi71/QXH/HTo05yd6B5+T+VHh1SqCpn/HpeGLDxkA+MK4+hr4eULB2/A8Jw==", "license": "MIT", "peerDependencies": { - "marked": ">=4 <18" + "marked": ">=4 <19" } }, "node_modules/marked-extended-tables": { @@ -10346,15 +10356,15 @@ } }, "node_modules/marked-gfm-heading-id": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-4.1.3.tgz", - "integrity": "sha512-aR0i63LmFbuxU/gAgrgz1Ir+8HK6zAIFXMlckeKHpV+qKbYaOP95L4Ux5Gi+sKmCZU5qnN2rdKpvpb7PnUBIWg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-4.1.4.tgz", + "integrity": "sha512-CspnvVfHSkb/znqdPS4jUR8HtCjq3M/DnrsJCrfLBLvdrgbemmoINKpeWKQYkBiXAoBGejw0cV7xzqrPdup3WA==", "license": "MIT", "dependencies": { "github-slugger": "^2.0.0" }, "peerDependencies": { - "marked": ">=13 <18" + "marked": ">=13 <19" } }, "node_modules/marked-nonbreaking-spaces": { @@ -10665,22 +10675,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/gcp-metadata": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", - "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/mongoose/node_modules/mongodb": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", @@ -11790,24 +11784,24 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" } }, "node_modules/react-frame-component": { diff --git a/package.json b/package.json index ffd9ac423..76b3aac75 100644 --- a/package.json +++ b/package.json @@ -86,10 +86,10 @@ ] }, "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-runtime": "^7.29.0", + "@babel/core": "^7.29.7", + "@babel/plugin-transform-runtime": "^7.29.7", "@babel/preset-env": "^7.29.5", - "@babel/preset-react": "^7.28.5", + "@babel/preset-react": "^7.29.7", "@babel/runtime": "^7.29.2", "@codemirror/autocomplete": "^6.20.2", "@codemirror/commands": "^6.10.3", @@ -103,7 +103,7 @@ "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.43.0", "@dmsnell/diff-match-patch": "^1.1.0", - "@googleapis/drive": "^20.1.0", + "@googleapis/drive": "^20.2.0", "@lezer/highlight": "^1.2.3", "@sanity/diff-match-patch": "^3.2.0", "@vitejs/plugin-react": "^5.1.2", @@ -114,24 +114,24 @@ "core-js": "^3.49.0", "cors": "^2.8.5", "create-react-class": "^15.7.0", - "dedent": "^1.7.1", + "dedent": "^1.7.2", "express": "^5.1.0", "express-async-handler": "^1.2.0", "express-static-gzip": "3.0.1", - "fflate": "^0.8.2", + "fflate": "^0.8.3", "fs-extra": "^11.3.5", "hash-wasm": "^4.12.0", - "idb-keyval": "^6.2.2", - "js-yaml": "^4.1.1", + "idb-keyval": "^6.2.5", + "js-yaml": "^4.2.0", "jwt-simple": "^0.5.6", "less": "^4.6.4", "lodash": "^4.18.1", "marked": "15.0.12", "marked-alignment-paragraphs": "^1.0.0", "marked-definition-lists": "^1.0.1", - "marked-emoji": "^2.0.2", + "marked-emoji": "^2.0.3", "marked-extended-tables": "^2.0.1", - "marked-gfm-heading-id": "^4.1.3", + "marked-gfm-heading-id": "^4.1.4", "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.4", @@ -142,8 +142,8 @@ "nanoid": "5.1.11", "nconf": "^0.13.0", "node": "^25.9.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-frame-component": "^5.3.2", "react-router": "^7.15.1", "sanitize-filename": "1.6.4", From c67ac534475ae84257e1245d49a350c97015c944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 03:23:50 +0000 Subject: [PATCH 48/52] Bump the prod-dependencies group across 1 directory with 4 updates Bumps the prod-dependencies group with 4 updates in the / directory: [@codemirror/autocomplete](https://github.com/codemirror/autocomplete), [@codemirror/view](https://github.com/codemirror/view), [mongoose](https://github.com/Automattic/mongoose) and [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router). Updates `@codemirror/autocomplete` from 6.20.2 to 6.20.3 - [Changelog](https://github.com/codemirror/autocomplete/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/autocomplete/commits) Updates `@codemirror/view` from 6.43.0 to 6.43.1 - [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/view/commits) Updates `mongoose` from 9.6.3 to 9.7.0 - [Release notes](https://github.com/Automattic/mongoose/releases) - [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md) - [Commits](https://github.com/Automattic/mongoose/compare/9.6.3...9.7.0) Updates `react-router` from 7.16.0 to 7.17.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.17.0/packages/react-router) --- updated-dependencies: - dependency-name: "@codemirror/autocomplete" dependency-version: 6.20.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: "@codemirror/view" dependency-version: 6.43.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-dependencies - dependency-name: mongoose dependency-version: 9.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-dependencies - dependency-name: react-router dependency-version: 7.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 32 ++++++++++++++++---------------- package.json | 8 ++++---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index da0a66e85..386a3b7e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@babel/preset-env": "^7.29.5", "@babel/preset-react": "^7.29.7", "@babel/runtime": "^7.29.2", - "@codemirror/autocomplete": "^6.20.2", + "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.10.3", "@codemirror/highlight": "^0.19.8", "@codemirror/lang-css": "^6.3.1", @@ -25,7 +25,7 @@ "@codemirror/language-data": "^6.5.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.6.0", - "@codemirror/view": "^6.43.0", + "@codemirror/view": "^6.43.1", "@dmsnell/diff-match-patch": "^1.1.0", "@googleapis/drive": "^20.2.0", "@lezer/highlight": "^1.2.3", @@ -62,14 +62,14 @@ "marked-variables": "^1.0.5", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^9.6.2", + "mongoose": "^9.7.0", "nanoid": "5.1.11", "nconf": "^0.13.0", "node": "^25.9.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-frame-component": "^5.3.2", - "react-router": "^7.15.1", + "react-router": "^7.17.0", "sanitize-filename": "1.6.4", "superagent": "^10.2.1" }, @@ -2090,9 +2090,9 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.20.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", - "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2570,9 +2570,9 @@ "license": "MIT" }, "node_modules/@codemirror/view": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", - "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "version": "6.43.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz", + "integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.6.0", @@ -10655,9 +10655,9 @@ } }, "node_modules/mongoose": { - "version": "9.6.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.3.tgz", - "integrity": "sha512-vI6dTTlQnfMCyyQ5TrvhG0bCRs4dq5e1uFNPtOOWsOhn0fSg8AoIHjfyyCYr8aybyvPs845dRHGxsC3w/fHcBA==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.7.0.tgz", + "integrity": "sha512-pkrLZ6U41pD4Ai0ju/FYL7o5I5k+rV3RZINQTG937hbhnLGKRuqqYm1Dlt/kTQ+M4FHijzV6JawzsdHKRGt7QA==", "license": "MIT", "dependencies": { "kareem": "3.3.0", @@ -11841,9 +11841,9 @@ } }, "node_modules/react-router": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz", - "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", diff --git a/package.json b/package.json index 76b3aac75..ee0256be9 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@babel/preset-env": "^7.29.5", "@babel/preset-react": "^7.29.7", "@babel/runtime": "^7.29.2", - "@codemirror/autocomplete": "^6.20.2", + "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.10.3", "@codemirror/highlight": "^0.19.8", "@codemirror/lang-css": "^6.3.1", @@ -101,7 +101,7 @@ "@codemirror/language-data": "^6.5.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.6.0", - "@codemirror/view": "^6.43.0", + "@codemirror/view": "^6.43.1", "@dmsnell/diff-match-patch": "^1.1.0", "@googleapis/drive": "^20.2.0", "@lezer/highlight": "^1.2.3", @@ -138,14 +138,14 @@ "marked-variables": "^1.0.5", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.30.1", - "mongoose": "^9.6.2", + "mongoose": "^9.7.0", "nanoid": "5.1.11", "nconf": "^0.13.0", "node": "^25.9.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-frame-component": "^5.3.2", - "react-router": "^7.15.1", + "react-router": "^7.17.0", "sanitize-filename": "1.6.4", "superagent": "^10.2.1" }, From e7ec8a2174240e67e36d8f952b313cd8212a0ccb Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 14 Jun 2026 22:34:07 +1200 Subject: [PATCH 49/52] Initial attempt at bug fix --- client/components/splitPane/splitPane.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/components/splitPane/splitPane.jsx b/client/components/splitPane/splitPane.jsx index 7cbfe2066..5eef6bd30 100644 --- a/client/components/splitPane/splitPane.jsx +++ b/client/components/splitPane/splitPane.jsx @@ -18,8 +18,7 @@ const SplitPane = (props)=>{ const [liveScroll, setLiveScroll] = useState(false); useEffect(()=>{ - const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY); - setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2); + handleResize(); setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true'); window.addEventListener('resize', handleResize); @@ -29,7 +28,10 @@ const SplitPane = (props)=>{ const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x))); //when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position - const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13))); + const handleResize = ()=>{ + const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY); + setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2); + }; const handleUp =(e)=>{ e.preventDefault(); From a645b614649e970d641b8f948f5e43c570c1bbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 17 Jun 2026 15:50:21 +0200 Subject: [PATCH 50/52] update comments --- server/admin.api.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/admin.api.js b/server/admin.api.js index 451e2aa83..1d9b5a2bd 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -38,7 +38,8 @@ export default function createAdminApi(vite) { throw { HBErrorCode: '52', code: 401, message: 'Access denied' }; } }; - + + // Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes const junkBrewsPipeline = [ { $match : { updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, @@ -49,12 +50,13 @@ export default function createAdminApi(vite) { { $limit: 300 } ]; + // Search for up to 500 unauthored brews that have not been viewed or updated in two years const lostBrewsPipeline = [ { $match: { authors: [], - updatedAt: { $lt: Moment().subtract(6, 'years').toDate() }, - lastViewed: { $lt: Moment().subtract(6, 'years').toDate() } + updatedAt: { $lt: Moment().subtract(2, 'years').toDate() }, + lastViewed: { $lt: Moment().subtract(2, 'years').toDate() } } }, { @@ -67,7 +69,6 @@ export default function createAdminApi(vite) { 'text' : { '$exists': true } }).lean().limit(10000).select('_id'); - // Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 }) .then((objs)=>res.json({ count: objs.length, brewCollection : objs })) @@ -77,7 +78,7 @@ export default function createAdminApi(vite) { }); }); - // Delete up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes + // Delete result of junkBrewsPipeline router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 }) .then((docs)=>{ @@ -91,7 +92,6 @@ export default function createAdminApi(vite) { }); }); - // Search for up to 300 unauthored brews that have not been viewed or updated in a year router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 }) .then((objs)=>res.json({ count: objs.length, brewCollection : objs })) @@ -101,7 +101,7 @@ export default function createAdminApi(vite) { }); }); - // Delete up to 300 unauthored brews that have not been viewed or updated in a year + // Delete result of lostBrewsPipeline router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 }) .then((docs)=>{ From 515a59c9decdedff57eeea0eb42bcf0f4ca84e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 17 Jun 2026 15:59:42 +0200 Subject: [PATCH 51/52] updated fields to have proper data --- server/admin.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/admin.api.js b/server/admin.api.js index 1d9b5a2bd..d0676f47f 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -45,7 +45,7 @@ export default function createAdminApi(vite) { updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } } }, - { $project: { _id: 1, textBinSize: { $binarySize: '$textBin' } } }, + { $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1} }, { $match: { textBinSize: { $lt: 140 } } }, { $limit: 300 } ]; From edf5a6f9e8e4237829978808ad141459a140db9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 17 Jun 2026 16:22:03 +0200 Subject: [PATCH 52/52] a few touches as gazook requested --- .../brewUtils/brewCleanup/brewCleanup.jsx | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index e2acb2a9d..7d6c34f55 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -5,71 +5,89 @@ import Moment from 'moment'; const BrewCleanup = ({})=>{ const [junkBrewCollection, setJunkBrewCollection] = useState([]); const [lostBrewCollection, setLostBrewCollection] = useState([]); - const [pending, setPending] = useState(false); + const [pendingJunk, setPendingJunk] = useState(false); + const [pendingLost, setPendingLost] = useState(false); const [error, setError] = useState(null); const find = async (type)=>{ - setPending(true); + if(type === 'junk') try { + setPendingJunk(true); const res = await request.get('/admin/cleanupJunk'); setJunkBrewCollection(res.body.brewCollection); } catch (err) { setError(err); } finally { - setPending(false); + setPendingJunk(false); } if(type === 'lost') try { - + setPendingLost(true); const res = await request.get('/admin/cleanupLost'); setLostBrewCollection(res.body.brewCollection); } catch (err) { setError(err); } finally { - setPending(false); + setPendingLost(false); } }; const cleanup = async (type)=>{ - setPending(true); if(type === 'junk') try { - console.log('deleting junk') + setPendingJunk(true); + console.log('deleting junk'); const res = await request.post('/admin/cleanupJunk'); } catch (err) { setError(err); } finally { - setPending(false); + setPendingJunk(false); setJunkBrewCollection([]); } if(type === 'lost') try { + setPendingLost(true); const res = await request.post('/admin/cleanupLost'); } catch (err) { setError(err); } finally { - setPending(false); + setPendingLost(false); setLostBrewCollection([]); } }; const renderBrewList = (type)=>{ - const brewList = type === 'lost' ? lostBrewCollection : junkBrewCollection; if(!brewList || brewList.length === 0) { - console.log(brewList); - return null; + return <> +

{`Results - No brews found` }

+
Title Last UpdateCreatedlast viewed Storage
{brew.title || 'No Title'} {Moment(brew.updatedAt).fromNow()}{brew.createdAt ? Moment(brew.createdAt).fromNow() : 'No creation date'}{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No creation date'} {brew.googleId ? 'Google' : 'Homebrewery'}
{brew.title || 'No Title'} {Moment(brew.updatedAt).fromNow()}{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No creation date'}{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'} {brew.googleId ? 'Google' : 'Homebrewery'}
+ + + + + + + + + + + + + +
TitleLast Updatelast viewedStorage
"No brews found"
+ ; } - - + console.log(type); + console.log(brewList); return <> -

{`Results - ${brewList.length} brews` }

+

{`Results - ${brewList.length} brews` }

@@ -91,35 +109,33 @@ const BrewCleanup = ({})=>{ - ; + })}
{Moment(brew.updatedAt).fromNow()} {brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'} {brew.googleId ? 'Google' : 'Homebrewery'}
; }; const renderFound = (type)=>{ - - if(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0) return
No Matching Brews found.
; + const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0); return
- - - Found {type === 'junk' ? junkBrewCollection.length : lostBrewCollection.length} Brews that could be removed. + } {renderBrewList(type)}
; }; const renderJunkBrewCleanup = ()=>{ return

Junk brews

-

Removes very short brews to tidy up the database

+

Queries unauthored brews that have not been viewed or
updated in 30 days and are shorter than 140 bytes (up to 300)