mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-06-22 04:58:40 +00:00
Merge branch 'master' into Open-Sans---Installed-vs-Google-Fonts
This commit is contained in:
@@ -17,8 +17,7 @@ import {
|
|||||||
crosshairCursor,
|
crosshairCursor,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
|
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
|
||||||
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
|
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { foldEffect } from '@codemirror/language';
|
|
||||||
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
||||||
import { languages } from '@codemirror/language-data';
|
import { languages } from '@codemirror/language-data';
|
||||||
import { css } from '@codemirror/lang-css';
|
import { css } from '@codemirror/lang-css';
|
||||||
@@ -38,8 +37,6 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
|||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
const highlightCompartment = new Compartment();
|
const highlightCompartment = new Compartment();
|
||||||
|
|
||||||
console.log(themes);
|
|
||||||
|
|
||||||
import { generalKeymap, markdownKeymap } from './customKeyMaps.js';
|
import { generalKeymap, markdownKeymap } from './customKeyMaps.js';
|
||||||
import foldOnPages from './customFolding.js';
|
import foldOnPages from './customFolding.js';
|
||||||
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
||||||
@@ -151,11 +148,14 @@ const CodeEditor = forwardRef(
|
|||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const viewRef = useRef(null);
|
const viewRef = useRef(null);
|
||||||
const docsRef = useRef({});
|
const docsRef = useRef({});
|
||||||
|
const tabRef = useRef(tab);
|
||||||
const prevTabRef = useRef(tab);
|
const prevTabRef = useRef(tab);
|
||||||
|
const scrollRef = useRef({});
|
||||||
|
const foldsRef = useRef({});
|
||||||
const pageMap = useRef([]);
|
const pageMap = useRef([]);
|
||||||
|
|
||||||
const recomputePages = (doc)=>{
|
const recomputePages = (doc)=>{
|
||||||
|
if(tab !== 'brewText') return;
|
||||||
const pages = [0];
|
const pages = [0];
|
||||||
const text = doc.toString();
|
const text = doc.toString();
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@@ -181,6 +181,14 @@ const CodeEditor = forwardRef(
|
|||||||
return page;
|
return page;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFoldRanges = (state)=>{
|
||||||
|
const folds = [];
|
||||||
|
state.field(foldState, false)?.between(0, state.doc.length, (from, to)=>{
|
||||||
|
folds.push({ from, to });
|
||||||
|
});
|
||||||
|
return folds;
|
||||||
|
};
|
||||||
|
|
||||||
const createExtensions = ({ onChange, language, editorTheme })=>{
|
const createExtensions = ({ onChange, language, editorTheme })=>{
|
||||||
const setEventListeners = EditorView.updateListener.of((update)=>{
|
const setEventListeners = EditorView.updateListener.of((update)=>{
|
||||||
if(update.docChanged) {
|
if(update.docChanged) {
|
||||||
@@ -267,11 +275,10 @@ const CodeEditor = forwardRef(
|
|||||||
ticking = true;
|
ticking = true;
|
||||||
requestAnimationFrame(()=>{
|
requestAnimationFrame(()=>{
|
||||||
const top = view.scrollDOM.scrollTop;
|
const top = view.scrollDOM.scrollTop;
|
||||||
|
scrollRef.current[tabRef.current] = top;
|
||||||
const block = view.lineBlockAtHeight(top);
|
const block = view.lineBlockAtHeight(top);
|
||||||
|
const page = findPageFromPos(block.from);
|
||||||
const page = findPageFromPos(block.from); // CHANGED
|
|
||||||
onViewChange(page);
|
onViewChange(page);
|
||||||
|
|
||||||
ticking = false;
|
ticking = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -286,12 +293,23 @@ const CodeEditor = forwardRef(
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const restoreFolds = (view, folds)=>{
|
||||||
|
if(!folds?.length) return;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
effects : folds.map((f)=>foldEffect.of(f))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
if(!view) return;
|
if(!view) return;
|
||||||
|
|
||||||
|
tabRef.current = tab;
|
||||||
const prevTab = prevTabRef.current;
|
const prevTab = prevTabRef.current;
|
||||||
|
|
||||||
|
foldsRef.current[prevTab] = getFoldRanges(view.state);
|
||||||
|
|
||||||
if(prevTab !== tab) {
|
if(prevTab !== tab) {
|
||||||
docsRef.current[prevTab] = view.state;
|
docsRef.current[prevTab] = view.state;
|
||||||
|
|
||||||
@@ -305,6 +323,16 @@ const CodeEditor = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.setState(nextState);
|
view.setState(nextState);
|
||||||
|
restoreFolds(view, foldsRef.current[tab]);
|
||||||
|
|
||||||
|
const savedScroll = scrollRef.current[tab];
|
||||||
|
|
||||||
|
if(savedScroll != null) {
|
||||||
|
requestAnimationFrame(()=>{
|
||||||
|
view.scrollDOM.scrollTop = savedScroll;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
prevTabRef.current = tab;
|
prevTabRef.current = tab;
|
||||||
}
|
}
|
||||||
view.focus();
|
view.focus();
|
||||||
|
|||||||
@@ -100,6 +100,10 @@
|
|||||||
vertical-align : sub;
|
vertical-align : sub;
|
||||||
color : rgb(123, 123, 15);
|
color : rgb(123, 123, 15);
|
||||||
}
|
}
|
||||||
|
.cm-strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-definitionList {
|
.cm-definitionList {
|
||||||
.cm-definitionTerm { color : rgb(96, 117, 143); }
|
.cm-definitionTerm { color : rgb(96, 117, 143); }
|
||||||
.cm-definitionColon:not(:has(.cm-comment)) {
|
.cm-definitionColon:not(:has(.cm-comment)) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const customTags = {
|
|||||||
definitionTerm : 'definitionTerm', // .cm-definitionTerm
|
definitionTerm : 'definitionTerm', // .cm-definitionTerm
|
||||||
definitionDesc : 'definitionDesc', // .cm-definitionDesc
|
definitionDesc : 'definitionDesc', // .cm-definitionDesc
|
||||||
definitionColon : 'definitionColon', // .cm-definitionColon
|
definitionColon : 'definitionColon', // .cm-definitionColon
|
||||||
|
strikethrough : 'strikethrough', // .cm-strikethrough
|
||||||
|
|
||||||
//CSS
|
//CSS
|
||||||
|
|
||||||
@@ -81,6 +82,23 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Strikethrough ---
|
||||||
|
if(/\~/.test(lineText)) {
|
||||||
|
const strikethroughRegex = /~(?!\s)(.+?)(?<!\s)~/g;
|
||||||
|
|
||||||
|
const match = strikethroughRegex.exec(lineText);
|
||||||
|
const type = customTags.strikethrough;
|
||||||
|
|
||||||
|
if(match) {
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
type,
|
||||||
|
from : match.index,
|
||||||
|
to : match.index + match[0].length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- single line def list ---
|
// --- single line def list ---
|
||||||
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
|
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
|
||||||
const match = singleLineRegex.exec(lineText);
|
const match = singleLineRegex.exec(lineText);
|
||||||
@@ -182,13 +200,13 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(lineText.includes('{') && lineText.includes('}')) {
|
if(lineText.includes('{') && lineText.includes('}')) {
|
||||||
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
|
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
|
||||||
let match;
|
let match;
|
||||||
while ((match = injectionRegex.exec(lineText)) !== null) {
|
while ((match = injectionRegex.exec(lineText)) !== null) {
|
||||||
tokens.push({
|
tokens.push({
|
||||||
line : lineNumber,
|
line : lineNumber,
|
||||||
from : match.index,
|
from : match.indices[1][0],
|
||||||
to : match.index + match[1].length,
|
to : match.indices[1][1],
|
||||||
type : customTags.injection,
|
type : customTags.injection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ import { keymap } from '@codemirror/view';
|
|||||||
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
|
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
|
||||||
import { Prec } from '@codemirror/state';
|
import { Prec } from '@codemirror/state';
|
||||||
|
|
||||||
|
const insertTab = (view)=>{
|
||||||
|
const { from, to } = view.state.selection.main;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes : { from, to, insert: ' ' },
|
||||||
|
selection : { anchor: from + 2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const indentLess = (view)=>{
|
const indentLess = (view)=>{
|
||||||
const { from, to } = view.state.selection.main;
|
const { from, to } = view.state.selection.main;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
@@ -17,48 +28,44 @@ const indentLess = (view)=>{
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapSelection = (prefix, suffix) => (view) => {
|
const wrapSelection = (prefix, suffix)=>(view)=>{
|
||||||
const { from, to } = view.state.selection.main;
|
const changes = [];
|
||||||
const selected = view.state.doc.sliceString(from, to);
|
|
||||||
|
|
||||||
let text, selection;
|
for (const range of view.state.selection.ranges) {
|
||||||
|
const { from, to } = range;
|
||||||
|
const selected = view.state.doc.sliceString(from, to);
|
||||||
|
|
||||||
if(from === to) {
|
let text;
|
||||||
text = prefix + suffix;
|
|
||||||
selection = { anchor: from + prefix.length, head: from + prefix.length };
|
if(from === to) { text = prefix + suffix; } else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
|
||||||
}
|
text = selected.slice(prefix.length, -suffix.length);
|
||||||
else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
|
} else {text = `${prefix}${selected}${suffix}`;}
|
||||||
text = selected.slice(prefix.length, -suffix.length);
|
|
||||||
selection = { anchor: from, head: from + text.length };
|
changes.push({ from, to, insert: text });
|
||||||
}
|
|
||||||
else {
|
|
||||||
text = `${prefix}${selected}${suffix}`;
|
|
||||||
selection = { anchor: from, head: from + text.length };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes : { from, to, insert: text },
|
changes
|
||||||
selection
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeNbsp = (view) => {
|
const makeNbsp = (view)=>{
|
||||||
const { from } = view.state.selection.main;
|
const { from } = view.state.selection.main;
|
||||||
|
|
||||||
const prev2 = from >= 2
|
const prev2 = from >= 2
|
||||||
? view.state.doc.sliceString(from - 2, from)
|
? view.state.doc.sliceString(from - 2, from)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
|
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes : { from, to: from, insert },
|
changes : { from, to: from, insert },
|
||||||
selection : { anchor: from + insert.length },
|
selection : { anchor: from + insert.length },
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeSpace = (view)=>{
|
const makeSpace = (view)=>{
|
||||||
@@ -162,7 +169,7 @@ const newPage = (view)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generalKeymap = Prec.high(keymap.of([
|
export const generalKeymap = Prec.high(keymap.of([
|
||||||
{ key: 'Tab', run: indentMore },
|
{ key: 'Tab', run: insertTab },
|
||||||
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
|
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
|
||||||
{ key: 'Mod-Shift-z', run: redo },
|
{ key: 'Mod-Shift-z', run: redo },
|
||||||
{ key: 'Mod-y', run: redo },
|
{ key: 'Mod-y', run: redo },
|
||||||
|
|||||||
@@ -41,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);
|
||||||
@@ -220,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;
|
||||||
|
|
||||||
@@ -232,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,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,
|
||||||
|
|||||||
@@ -30,18 +30,17 @@ import cm5Themes from 'codemirror-5-themes';
|
|||||||
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
||||||
|
|
||||||
const themeNames = Object.entries(themes)
|
const themeNames = Object.entries(themes)
|
||||||
.filter(([name, value]) =>
|
.filter(([name, value])=>Array.isArray(value) &&
|
||||||
Array.isArray(value) &&
|
|
||||||
!name.endsWith('Init') &&
|
!name.endsWith('Init') &&
|
||||||
!name.endsWith('Style')
|
!name.endsWith('Style')
|
||||||
)
|
)
|
||||||
.map(([name]) => name);
|
.map(([name])=>name);
|
||||||
|
|
||||||
const EditorThemes = [
|
const EditorThemes = [
|
||||||
'default',
|
'default',
|
||||||
...themeNames
|
...themeNames
|
||||||
.filter(name => name !== 'default')
|
.filter((name)=>name !== 'default')
|
||||||
.sort((a, b) => a.localeCompare(b))
|
.sort((a, b)=>a.localeCompare(b))
|
||||||
];
|
];
|
||||||
|
|
||||||
const execute = function(val, props){
|
const execute = function(val, props){
|
||||||
|
|||||||
@@ -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>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const EditPage = (props)=>{
|
|||||||
|
|
||||||
const handleControlKeys = (e)=>{
|
const handleControlKeys = (e)=>{
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
if(e.keyCode === 83) trySaveRef.current(true);
|
if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle);
|
||||||
if(e.keyCode === 80) printCurrentBrew();
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
if([83, 80].includes(e.keyCode)) {
|
if([83, 80].includes(e.keyCode)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -118,13 +118,9 @@ const EditPage = (props)=>{
|
|||||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||||
setUnsavedChanges(hasChange);
|
setUnsavedChanges(hasChange);
|
||||||
|
|
||||||
if(autoSaveEnabled) trySave(false, hasChange);
|
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
|
||||||
}, [currentBrew]);
|
}, [currentBrew]);
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
trySave(true);
|
|
||||||
}, [saveGoogle]);
|
|
||||||
|
|
||||||
const handleSplitMove = ()=>{
|
const handleSplitMove = ()=>{
|
||||||
editorRef.current?.update();
|
editorRef.current?.update();
|
||||||
};
|
};
|
||||||
@@ -183,11 +179,13 @@ const EditPage = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleGoogleStorage = ()=>{
|
const toggleGoogleStorage = ()=>{
|
||||||
|
const newSaveGoogle = !saveGoogle;
|
||||||
setSaveGoogle((prev)=>!prev);
|
setSaveGoogle((prev)=>!prev);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
trySave(true, true, newSaveGoogle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const trySave = (immediate = false, hasChanges = true)=>{
|
const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
|
||||||
clearTimeout(saveTimeout.current);
|
clearTimeout(saveTimeout.current);
|
||||||
if(isSaving) return;
|
if(isSaving) return;
|
||||||
if(!hasChanges && !immediate) return;
|
if(!hasChanges && !immediate) return;
|
||||||
@@ -196,7 +194,7 @@ const EditPage = (props)=>{
|
|||||||
saveTimeout.current = setTimeout(async ()=>{
|
saveTimeout.current = setTimeout(async ()=>{
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
await save(currentBrew, saveGoogle)
|
await save(currentBrew, saveToGoogle)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
setError(err);
|
setError(err);
|
||||||
});
|
});
|
||||||
@@ -216,7 +214,7 @@ const EditPage = (props)=>{
|
|||||||
const brewToSave = {
|
const brewToSave = {
|
||||||
...brew,
|
...brew,
|
||||||
text : brew.text.normalize('NFC'),
|
text : brew.text.normalize('NFC'),
|
||||||
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
|
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1,
|
||||||
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
||||||
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
|
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
|
||||||
textBin : undefined,
|
textBin : undefined,
|
||||||
@@ -314,7 +312,7 @@ const EditPage = (props)=>{
|
|||||||
|
|
||||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||||
if(unsavedChanges)
|
if(unsavedChanges)
|
||||||
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
return <Nav.item className='save' onClick={()=>trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||||
|
|
||||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||||
if(autoSaveEnabled)
|
if(autoSaveEnabled)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ const NewPage = (props)=>{
|
|||||||
const updatedBrew = { ...currentBrew };
|
const updatedBrew = { ...currentBrew };
|
||||||
splitTextStyleAndMetadata(updatedBrew);
|
splitTextStyleAndMetadata(updatedBrew);
|
||||||
|
|
||||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm;
|
||||||
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
|
|||||||
Generated
+712
-695
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -88,10 +88,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.29.2",
|
"@babel/preset-env": "^7.29.5",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@babel/runtime": "^7.29.2",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@codemirror/autocomplete": "^6.20.1",
|
"@codemirror/autocomplete": "^6.20.2",
|
||||||
"@codemirror/commands": "^6.10.3",
|
"@codemirror/commands": "^6.10.3",
|
||||||
"@codemirror/highlight": "^0.19.8",
|
"@codemirror/highlight": "^0.19.8",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"@codemirror/language-data": "^6.5.2",
|
"@codemirror/language-data": "^6.5.2",
|
||||||
"@codemirror/search": "^6.6.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
"@codemirror/state": "^6.6.0",
|
"@codemirror/state": "^6.6.0",
|
||||||
"@codemirror/view": "^6.40.0",
|
"@codemirror/view": "^6.43.0",
|
||||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||||
"@googleapis/drive": "^20.1.0",
|
"@googleapis/drive": "^20.1.0",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
@@ -117,9 +117,9 @@
|
|||||||
"dedent": "^1.7.1",
|
"dedent": "^1.7.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "3.0.0",
|
"express-static-gzip": "3.0.1",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.5",
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
@@ -138,31 +138,31 @@
|
|||||||
"marked-variables": "^1.0.5",
|
"marked-variables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^9.3.3",
|
"mongoose": "^9.6.2",
|
||||||
"nanoid": "5.1.7",
|
"nanoid": "5.1.11",
|
||||||
"nconf": "^0.13.0",
|
"nconf": "^0.13.0",
|
||||||
"node": "^25.9.0",
|
"node": "^25.9.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.6",
|
||||||
"react-frame-component": "^5.3.2",
|
"react-frame-component": "^5.3.2",
|
||||||
"react-router": "^7.14.0",
|
"react-router": "^7.15.1",
|
||||||
"sanitize-filename": "1.6.4",
|
"sanitize-filename": "1.6.4",
|
||||||
"superagent": "^10.2.1"
|
"superagent": "^10.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^5.0.1",
|
"@stylistic/stylelint-plugin": "^5.0.1",
|
||||||
"babel-jest": "^30.3.0",
|
"babel-jest": "^30.4.1",
|
||||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||||
"eslint": "9.7",
|
"eslint": "9.7",
|
||||||
"eslint-plugin-jest": "^29.15.1",
|
"eslint-plugin-jest": "^29.15.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.3.0",
|
"jest": "^30.4.2",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^17.6.0",
|
"stylelint": "^17.11.1",
|
||||||
"stylelint-config-recess-order": "^7.7.0",
|
"stylelint-config-recess-order": "^7.7.0",
|
||||||
"stylelint-config-recommended": "^18.0.0",
|
"stylelint-config-recommended": "^18.0.0",
|
||||||
"supertest": "^7.1.4",
|
"supertest": "^7.1.4",
|
||||||
|
|||||||
+1
-1
@@ -593,7 +593,7 @@ export default async function createApp(vite) {
|
|||||||
|
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
'<head>',
|
'<head>',
|
||||||
`<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`
|
()=>{ return `<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`; }
|
||||||
);
|
);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
+18
-11
@@ -32,20 +32,20 @@ const isStaticTheme = (renderer, themeName)=>{
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
|
|
||||||
const migrateSystemsToTags = (brew) => {
|
const migrateSystemsToTags = (brew)=>{
|
||||||
if (!('systems' in brew)) return brew;
|
if(!('systems' in brew)) return brew;
|
||||||
|
|
||||||
if (!Array.isArray(brew.systems) || brew.systems.length === 0) {
|
if(!Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||||
brew.systems = undefined;
|
brew.systems = undefined;
|
||||||
return brew;
|
return brew;
|
||||||
}
|
}
|
||||||
const systemMap = {
|
const systemMap = {
|
||||||
'5e': 'system:D&D 5e',
|
'5e' : 'system:D&D 5e',
|
||||||
'4e': 'system:D&D 4e',
|
'4e' : 'system:D&D 4e',
|
||||||
'3.5e': 'system:D&D 3.5e',
|
'3.5e' : 'system:D&D 3.5e',
|
||||||
'Pathfinder': 'system:Pathfinder 2e'
|
'Pathfinder' : 'system:Pathfinder 2e'
|
||||||
};
|
};
|
||||||
const systemTags = brew.systems.map(s => systemMap[s]);
|
const systemTags = brew.systems.map((s)=>systemMap[s]);
|
||||||
brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
|
brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
|
||||||
|
|
||||||
brew.systems = undefined;
|
brew.systems = undefined;
|
||||||
@@ -397,17 +397,24 @@ const api = {
|
|||||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result = [];
|
||||||
try {
|
try {
|
||||||
const patches = parsePatch(brewFromClient.patches);
|
const patches = parsePatch(brewFromClient.patches);
|
||||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||||
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
result = applyPatches(patches, encodeURI(brewFromServer.text));
|
||||||
if(patchedResult != brewFromClient.text)
|
const failedPatches = patches.map((patch, index)=>{if(!result[1][index]){ return patch; }});
|
||||||
|
if(failedPatches > 0){
|
||||||
|
throw (`Patch failure: ${failedPatches}/${result[1].length} did not apply`);
|
||||||
|
}
|
||||||
|
if(decodeURI(result[0]) != brewFromClient.text){
|
||||||
throw ('Patches did not apply cleanly, text mismatch detected');
|
throw ('Patches did not apply cleanly, text mismatch detected');
|
||||||
|
}
|
||||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||||
console.error('Failed to apply patches:', {
|
console.error('Failed to apply patches:', {
|
||||||
//patches : brewFromClient.patches,
|
// patches : brewFromClient.patches,
|
||||||
|
// result : result,
|
||||||
brewId : brewFromClient.editId || 'unknown',
|
brewId : brewFromClient.editId || 'unknown',
|
||||||
error : err
|
error : err
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : { type: [String], index: true },
|
tags : { type: [String], index: true },
|
||||||
systems : { type: [String], default: undefined },
|
systems : { type: [String], default: undefined },
|
||||||
lang : { type: String, default: 'en', index: true },
|
lang : { type: String, default: 'en', index: true },
|
||||||
renderer : { type: String, default: '', index: true },
|
renderer : { type: String, default: '', index: true },
|
||||||
authors : { type: [String], index: true },
|
authors : { type: [String], index: true },
|
||||||
|
|||||||
+56
-9
@@ -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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,9 +181,35 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
|
|||||||
// Char-level diff
|
// Char-level diff
|
||||||
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
||||||
if(clientText[i] !== serverText[i]) {
|
if(clientText[i] !== serverText[i]) {
|
||||||
|
const getMismatchContext = (text, index, name, size = 10)=>{
|
||||||
|
const lower = Math.max(index - size, 0);
|
||||||
|
const upper = Math.min(index + size, text.length);
|
||||||
|
const slice = `${JSON.stringify(text.slice(lower, index)).slice(1, -1)}\u001B[31m${JSON.stringify(text[i]).slice(1, -1)}\u001B[0m${JSON.stringify(text.slice(index+1, upper)).slice(1, -1)}`;
|
||||||
|
const lineNo = text.slice(0, index).split('\n').length;
|
||||||
|
const code = `U+${text.charCodeAt(i).toString(16).toUpperCase()}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
lineNo,
|
||||||
|
code,
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
slice
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const boundSize = 10;
|
||||||
|
|
||||||
|
const clientContext = getMismatchContext(clientText, i, 'Client', boundSize);
|
||||||
|
const serverContext = getMismatchContext(serverText, i, 'Server', boundSize);
|
||||||
|
|
||||||
|
const logContext = (context)=>{
|
||||||
|
console.log(` ${context.name} - line ${context.lineNo} : (${context.code})\t${context.slice}`);
|
||||||
|
};
|
||||||
|
|
||||||
console.log(`Char mismatch at index ${i}:`);
|
console.log(`Char mismatch at index ${i}:`);
|
||||||
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
logContext(clientContext);
|
||||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
logContext(serverContext);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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}"`;
|
||||||
|
|
||||||
|
|||||||
@@ -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 = '{position:absolute}';
|
const source = '{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 = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
const source = ` {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 = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
const source = ` {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 = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`;
|
const source = ` {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>`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -315,21 +315,21 @@ describe('Normal Links and Images', ()=>{
|
|||||||
const source = ``;
|
const source = ``;
|
||||||
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 !';
|
const source = 'An image !';
|
||||||
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 = `{width:100px}`;
|
const source = `{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='';
|
const source='';
|
||||||
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='';
|
const source='';
|
||||||
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='';
|
const source='';
|
||||||
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='{height=20%,width=20%}';
|
const source='{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>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ export default {
|
|||||||
edge : (side = 'bottom')=>{
|
edge : (side = 'bottom')=>{
|
||||||
const styles = ()=>{
|
const styles = ()=>{
|
||||||
switch (side) {
|
switch (side) {
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
return `{width:100%,bottom:0%}`
|
return `{width:100%,bottom:0%}`;
|
||||||
break;
|
break;
|
||||||
case 'top':
|
case 'top':
|
||||||
return `{width:100%,top:0%}`
|
return `{width:100%,top:0%}`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return `{height:100%}`
|
return `{height:100%}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const rotation = {
|
const rotation = {
|
||||||
'bottom' : 0,
|
'bottom' : 0,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default EditorView.theme({
|
|||||||
'.cm-strong' : { color: '#309dd2', fontWeight: 'bold' },
|
'.cm-strong' : { color: '#309dd2', fontWeight: 'bold' },
|
||||||
'.cm-em' : { fontStyle: 'italic' },
|
'.cm-em' : { fontStyle: 'italic' },
|
||||||
'.cm-keyword' : { color: '#fff' },
|
'.cm-keyword' : { color: '#fff' },
|
||||||
'.cm-atom, .cm-value, .cm-color' : { color: '#c1939a' },
|
'.cm-atom, .cm-value, .cm-color' : { color: '#c1939a' },
|
||||||
'.cm-number' : { color: '#2986cc' },
|
'.cm-number' : { color: '#2986cc' },
|
||||||
'.cm-def' : { color: '#2986cc' },
|
'.cm-def' : { color: '#2986cc' },
|
||||||
'.cm-list' : { color: '#3cbf30' },
|
'.cm-list' : { color: '#3cbf30' },
|
||||||
|
|||||||
Reference in New Issue
Block a user