0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-06-22 04:58:40 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-cm-features

This commit is contained in:
Víctor Losada Hernández
2026-05-23 09:02:12 +02:00
13 changed files with 108 additions and 37 deletions
+11 -4
View File
@@ -29,7 +29,6 @@ const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent` const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head> <!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' /> <link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<link href="${brewRendererStylesUrl}" rel="stylesheet" /> <link href="${brewRendererStylesUrl}" rel="stylesheet" />
<link href="${headerNavStylesUrl}" rel="stylesheet" /> <link href="${headerNavStylesUrl}" rel="stylesheet" />
@@ -42,6 +41,7 @@ const BrewPage = (props)=>{
props = { props = {
contents : '', contents : '',
index : 0, index : 0,
hoisted : false,
...props ...props
}; };
const pageRef = useRef(null); const pageRef = useRef(null);
@@ -221,7 +221,8 @@ const BrewRenderer = (props)=>{
} }
}; };
const renderPages = ()=>{ const renderPages = (checkHoists = false)=>{
if(props.errors && props.errors.length) if(props.errors && props.errors.length)
return renderedPages; return renderedPages;
@@ -233,10 +234,16 @@ const BrewRenderer = (props)=>{
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
_.forEach(rawPages, (page, index)=>{ _.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ 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 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; return renderedPages;
}; };
@@ -276,7 +283,7 @@ const BrewRenderer = (props)=>{
window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash)); window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash));
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame 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)=>({ setState((prevState)=>({
...prevState, ...prevState,
isMounted : true, isMounted : true,
+18 -2
View File
@@ -1,9 +1,25 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import Nav from './nav.jsx'; import Nav from './nav.jsx';
import { printCurrentBrew } from '@shared/helpers.js'; import { printCurrentBrew } from '@shared/helpers.js';
export default function(){ 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 <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'> return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF {printing ? 'loading' : 'get PDF'}
</Nav.item>; </Nav.item>;
}; };
-4
View File
@@ -5,10 +5,6 @@
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" /> content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link
href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700"
rel="stylesheet"
type="text/css" />
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
+28 -7
View File
@@ -105,14 +105,35 @@ const splitTextStyleAndMetadata = (brew)=>{
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : []; if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
}; };
const printCurrentBrew = ()=>{ const printCurrentBrew = async ()=>{
if(window.typeof !== 'undefined') { if(window.typeof !== 'undefined') {
window.frames['BrewRenderer'].contentWindow.print(); // fire a custom event for the print cycle
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear document.dispatchEvent(new CustomEvent('print:startprep'));
const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0); try {
node.style.display='none'; const iframeDoc = window.frames['BrewRenderer'].contentDocument;
node.offsetHeight; // accessing this is enough to trigger a reflow
node.style.display=''; // 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 = 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'));
}
} }
}; };
+1 -1
View File
@@ -83,7 +83,7 @@ renderer.image = function (token) {
if(href === null) if(href === null)
return text; return text;
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`; let out = `<img loading="lazy" src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
if(title) if(title)
out += ` title="${title}"`; out += ` title="${title}"`;
+1 -8
View File
@@ -4,14 +4,7 @@
@import './animations.less'; @import './animations.less';
@import './colors.less'; @import './colors.less';
@import './tooltip.less'; @import './tooltip.less';
@font-face { @import './fonts/fonts.css';
font-family : 'CodeLight';
src : url('./CODE Light.otf') format('opentype');
}
@font-face {
font-family : 'CodeBold';
src : url('./CODE Bold.otf') format('opentype');
}
html,body, #reactRoot { html,body, #reactRoot {
height : 100vh; height : 100vh;
min-height : 100vh; min-height : 100vh;
+38
View File
@@ -0,0 +1,38 @@
/* open-sans-latin-wght-normal */
@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;
}
/*
* 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;
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;
}
@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;
}
+4 -4
View File
@@ -324,7 +324,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image element with injected style', function() { it('Renders an image element with injected style', function() {
const source = '![alt text](https://i.imgur.com/hMna6G0.png){position:absolute}'; const source = '![alt text](https://i.imgur.com/hMna6G0.png){position:absolute}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute;" src="https://i.imgur.com/hMna6G0.png" alt="alt text"></p>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute;" loading="lazy" src="https://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
}); });
it('Renders an element modified by only the first of two consecutive injections', function() { it('Renders an element modified by only the first of two consecutive injections', function() {
@@ -343,19 +343,19 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image with added attributes', function() { 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 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(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" loading="lazy" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
}); });
it('Renders an image with "=" in the url, and added attributes', function() { 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 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(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png?auth=12345&height=1024); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png?auth=12345&height=1024); position:absolute; bottom:20px; left:130px; width:220px;" loading="lazy" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
}); });
it('Renders an image and added attributes with "=" in the value, ', function() { 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 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(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" loading="lazy" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`);
}); });
}); });
+7 -7
View File
@@ -315,21 +315,21 @@ describe('Normal Links and Images', ()=>{
const source = `![alt text](url)`; const source = `![alt text](url)`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img src="url" alt="alt text" style="--HB_src:url(url);"></p>`.trimReturns()); <p><img loading="lazy" src="url" alt="alt text" style="--HB_src:url(url);"></p>`.trimReturns());
}); });
it('Renders normal images with a title', function() { it('Renders normal images with a title', function() {
const source = 'An image ![alt text](url "and title")!'; const source = 'An image ![alt text](url "and title")!';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>An image <img src="url" alt="alt text" style="--HB_src:url(url);" title="and title">!</p>`.trimReturns()); <p>An image <img loading="lazy" src="url" alt="alt text" style="--HB_src:url(url);" title="and title">!</p>`.trimReturns());
}); });
it('Applies curly injectors to images', function() { it('Applies curly injectors to images', function() {
const source = `![alt text](url){width:100px}`; const source = `![alt text](url){width:100px}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img style="--HB_src:url(url); width:100px;" src="url" alt="alt text"></p>`.trimReturns()); <p><img style="--HB_src:url(url); width:100px;" loading="lazy" src="url" alt="alt text"></p>`.trimReturns());
}); });
it('Renders normal links', function() { it('Renders normal links', function() {
@@ -438,25 +438,25 @@ describe('Regression Tests', ()=>{
it('Handle Extra spaces in image alt-text 1', function(){ it('Handle Extra spaces in image alt-text 1', function(){
const source='![ where is my image??](http://i.imgur.com/hMna6G0.png)'; const source='![ where is my image??](http://i.imgur.com/hMna6G0.png)';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>'); expect(rendered).toBe('<p><img loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
}); });
it('Handle Extra spaces in image alt-text 2', function(){ it('Handle Extra spaces in image alt-text 2', function(){
const source='![where is my image??](http://i.imgur.com/hMna6G0.png)'; const source='![where is my image??](http://i.imgur.com/hMna6G0.png)';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>'); expect(rendered).toBe('<p><img loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
}); });
it('Handle Extra spaces in image alt-text 3', function(){ it('Handle Extra spaces in image alt-text 3', function(){
const source='![where is my image?? ](http://i.imgur.com/hMna6G0.png)'; const source='![where is my image?? ](http://i.imgur.com/hMna6G0.png)';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>'); expect(rendered).toBe('<p><img loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
}); });
it('Handle Extra spaces in image alt-text 4', function(){ 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 source='![where is my image??](http://i.imgur.com/hMna6G0.png){height=20%,width=20%}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered).toBe('<p><img style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" height=\"20%\" width=\"20%\"></p>'); expect(rendered).toBe('<p><img style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\" loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" height=\"20%\" width=\"20%\"></p>');
}); });
}); });