Merge branch 'master' into hbfm

This commit is contained in:
David Bolack
2026-08-25 13:14:53 -05:00
7 changed files with 581 additions and 369 deletions
+264 -278
View File
@@ -1,8 +1,6 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
import './editor.less'; import './editor.less';
import React from 'react'; import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import createReactClass from 'create-react-class';
import _ from 'lodash';
import dedent from 'dedent'; import dedent from 'dedent';
import CodeEditor from '@components/codeEditor/codeEditor.jsx'; import CodeEditor from '@components/codeEditor/codeEditor.jsx';
@@ -18,12 +16,8 @@ import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const EditorThemes = Object.entries(themes) const EditorThemes = Object.entries(themes)
.filter(([name, value])=>Array.isArray(value) && .filter(([name, value])=>Array.isArray(value) && !name.endsWith('Init') && !name.endsWith('Style'))
!name.endsWith('Init') && .map(([name])=>name);
!name.endsWith('Style')
)
.map(([name])=>name);
//const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; //const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; //const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -45,306 +39,298 @@ const DEFAULT_SNIPPET_TEXT = dedent`
let isJumping = false; let isJumping = false;
let jumpSource = null; let jumpSource = null;
const Editor = createReactClass({ const Editor = forwardRef(
displayName : 'Editor', (
getDefaultProps : function() { {
return { brew = {},
brew : {
text : '',
style : ''
},
onBrewChange : ()=>{}, onBrewChange = ()=>{},
reportError : ()=>{}, reportError = ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange = ()=>{},
onViewPageChange : ()=>{}, onViewPageChange = ()=>{},
editorTheme : 'default', editorTheme = 'default',
renderer : 'legacy', renderer = 'legacy',
currentEditorCursorPageNum : 1, moveBrew,
currentEditorViewPageNum : 1, moveSource,
currentBrewRendererPageNum : 1, liveScroll,
};
},
getInitialState : function() {
return {
editorTheme : this.props.editorTheme,
view : 'text', //'text', 'style', 'meta', 'snippet'
snippetBarHeight : 26,
};
},
editor : React.createRef(null), setMoveArrows,
codeEditor : React.createRef(null), updateBrew,
showEditButtons,
themeBundle,
userThemes,
isText : function() {return this.state.view == 'text';}, currentEditorCursorPageNum = 1,
isStyle : function() {return this.state.view == 'style';}, currentEditorViewPageNum = 1,
isMeta : function() {return this.state.view == 'meta';}, currentBrewRendererPageNum = 1,
isSnip : function() {return this.state.view == 'snippet';}, },
ref,
)=>{
const [currentEditorTheme, setEditorTheme] = useState(editorTheme);
const [view, setView] = useState('text'); // 'text', 'style', 'meta', 'snippet'
const [snippetBarHeight, setSnippetBarHeight] = useState(26);
componentDidMount : function() { const editor = useRef(null);
const codeEditor = useRef(null);
const throttleBrewMove = useRef(null);
const brewRenderer = document.getElementById('BrewRenderer'); const isText = ()=>isView('text');
brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', this.handleControlKeys); const isStyle = ()=>isView('style');
document.addEventListener('keydown', this.handleControlKeys); const isMeta = ()=>isView('meta');
const isSnip = ()=>isView('snippet');
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const isView = (name)=>view === name;
if(editorTheme && EditorThemes.includes(editorTheme)) {
this.setState({ editorTheme });
} else {
this.setState({ editorTheme: 'default' });
}
const snippetBar = document.querySelector('.editor > .snippetBar');
if(!snippetBar) return;
this.resizeObserver = new ResizeObserver((entries)=>{ useEffect(()=>{
const height = document.querySelector('.editor > .snippetBar').offsetHeight; const brewRenderer = document.getElementById('BrewRenderer');
this.setState({ snippetBarHeight: height }); brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', handleControlKeys);
}); document.addEventListener('keydown', handleControlKeys);
this.resizeObserver.observe(snippetBar); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
}, if(editorTheme && EditorThemes.includes(editorTheme)) setEditorTheme(editorTheme); else setEditorTheme('default');
const snippetBar = document.querySelector('.editor > .snippetBar');
if(!snippetBar) return;
componentDidUpdate : function(prevProps, prevState, snapshot) { const resizeObserver = new ResizeObserver((entries)=>{
const height = document.querySelector('.editor > .snippetBar').offsetHeight;
setSnippetBarHeight(height);
});
resizeObserver.observe(snippetBar);
if(prevProps.moveBrew !== this.props.moveBrew) return ()=>{
this.brewJump(); if(resizeObserver) resizeObserver.disconnect();
if(prevProps.moveSource !== this.props.moveSource)
this.sourceJump();
if(this.props.liveScroll) {
if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) {
this.sourceJump(this.props.currentBrewRendererPageNum, false);
} else if(prevProps.currentEditorViewPageNum !== this.props.currentEditorViewPageNum) {
this.brewJump(this.props.currentEditorViewPageNum, false);
} else if(prevProps.currentEditorCursorPageNum !== this.props.currentEditorCursorPageNum) {
this.brewJump(this.props.currentEditorCursorPageNum, false);
}
}
},
componentWillUnmount() {
if(this.resizeObserver) this.resizeObserver.disconnect();
},
handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39;
if(e.keyCode == RIGHTARROW_KEY) this.brewJump();
if(e.keyCode == LEFTARROW_KEY) this.sourceJump();
if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
e.stopPropagation();
e.preventDefault();
}
},
updateCurrentCursorPage : function(pageNumber) {
this.props.onCursorPageChange(pageNumber);
},
updateCurrentViewPage : function(pageNumber) {
this.props.onViewPageChange(pageNumber);
},
handleInject : function(injectText){
this.codeEditor.current?.injectText(injectText);
},
handleViewChange : function(newView){
this.props.setMoveArrows(newView === 'text');
this.setState({
view : newView
}, ()=>{
this.codeEditor.current?.focus();
});
},
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window || !this.isText() || isJumping || jumpSource === 'source')
return;
// Get current brewRenderer scroll position and calculate target position
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
jumpSource = null;
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
};
isJumping = true;
jumpSource = 'brew';
checkIfScrollComplete();
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
if(smooth) {
const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling
const bounceDelay = 100;
const scrollDelay = 500;
if(!this.throttleBrewMove) {
this.throttleBrewMove = _.throttle((currentPos, bouncePos, targetPos)=>{
brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' });
setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
}; };
this.throttleBrewMove(currentPos, bouncePos, targetPos); }, []);
} else {
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' });
}
},
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){ useEffect(()=>{ if(moveBrew) brewJump(); }, [moveBrew]);
if(!this.isText() || isJumping || jumpSource === 'brew') useEffect(()=>{ if(moveSource) sourceJump(); }, [moveSource]);
return; useEffect(()=>{ if(liveScroll) sourceJump(currentBrewRendererPageNum, false); }, [currentBrewRendererPageNum, liveScroll]);
useEffect(()=>{ if(liveScroll) brewJump(currentEditorViewPageNum, false); }, [currentEditorViewPageNum, liveScroll]);
useEffect(()=>{ if(liveScroll) brewJump(currentEditorCursorPageNum, false); }, [currentEditorCursorPageNum, liveScroll]);
const editor = this.codeEditor.current; const handleControlKeys = (e)=>{
if(!editor) return; if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
jumpSource = 'source'; const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39;
if(e.keyCode == RIGHTARROW_KEY) brewJump();
if(e.keyCode == LEFTARROW_KEY) sourceJump();
if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
e.stopPropagation();
e.preventDefault();
}
};
editor.scrollToPage(targetPage); const updateCurrentCursorPage = (pageNumber)=>{
setTimeout(()=>{ onCursorPageChange(pageNumber);
jumpSource = null; };
}, 200);
},
//Called when there are changes to the editor's dimensions const updateCurrentViewPage = (pageNumber)=>{
update : function(){}, onViewPageChange(pageNumber);
};
updateEditorTheme : function(newTheme){ const handleInject = (injectText)=>{
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); codeEditor.current?.injectText(injectText);
this.setState({ };
editorTheme : newTheme
});
},
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory const handleViewChange = (newView)=>{
rerenderParent : function (){ setMoveArrows(newView === 'text');
this.forceUpdate(); setView(newView);
}, };
useEffect(()=>{
codeEditor.current?.focus();
}, [view]);
renderEditor : function(){ const brewJump = (targetPage = currentEditorCursorPageNum, smooth = true)=>{
if(this.isText()){ if(!window || !isText() || isJumping || jumpSource === 'source') return;
return <>
<CodeEditor key='codeEditor'
ref={this.codeEditor}
language='gfm'
tab='brewText'
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onBrewChange('text')}
onCursorChange={(page)=>this.updateCurrentCursorPage(page)}
onViewChange={(page)=>this.updateCurrentViewPage(page)}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/>
</>;
}
if(this.isStyle()){
return <>
<CodeEditor key='codeEditor'
ref={this.codeEditor}
language='css'
tab='brewStyles'
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/>
</>;
}
if(this.isMeta()){
return <>
<CodeEditor key='codeEditor'
view={this.state.view}
style={{ display: 'none' }}/>
<MetadataEditor
metadata={this.props.brew}
themeBundle={this.props.themeBundle}
onChange={this.props.onBrewChange('metadata')}
reportError={this.props.reportError}
userThemes={this.props.userThemes}/>
</>;
}
if(this.isSnip()){
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
return <>
<CodeEditor key='codeEditor'
ref={this.codeEditor}
language='gfm'
tab='brewSnippets'
view={this.state.view}
value={this.props.brew.snippets}
onChange={this.props.onBrewChange('snippets')}
enableFolding={true}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - 25px)` }}/>
</>;
}
},
redo : function(){ const brewRenderer =
return this.codeEditor.current?.redo(); window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
},
historySize : function(){ const currentPos = brewRenderer.scrollTop;
return this.codeEditor.current?.historySize();
},
undo : function(){ const targetPos = window.frames['BrewRenderer'].contentDocument
return this.codeEditor.current?.undo(); .getElementById(`p${targetPage}`)
}, .getBoundingClientRect().top;
foldCode : function() { let scrollingTimeout;
return this.codeEditor.current?.foldAll();
}, const checkIfScrollComplete = ()=>{// Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout);// Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
jumpSource = null;
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
}, 150);// If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
};
isJumping = true;
jumpSource = 'brew';
checkIfScrollComplete();
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
if(smooth) {
const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling
const now = Date.now();
if(now - throttleBrewMove.current >= 500) {
throttleBrewMove.current = now;
brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' });
setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, 100);
}
} else {
brewRenderer.scrollTo({ top : currentPos + targetPos, behavior : 'instant', block : 'start',
});
}
};
const sourceJump = (targetPage = currentBrewRendererPageNum, smooth = true)=>{
if(!isText() || isJumping || jumpSource === 'brew') return;
if(!codeEditor.current) return;
jumpSource = 'source';
codeEditor.current.scrollToPage(targetPage);
setTimeout(()=>{
jumpSource = null;
}, 200);
};
const updateEditorTheme = (newTheme)=>{
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
setEditorTheme(newTheme);
};
const renderEditor = ()=>{
if(isText()) {
return (
<>
<CodeEditor
key='codeEditor'
ref={codeEditor}
language='gfm'
tab='brewText'
view={view}
value={brew.text}
onChange={onBrewChange('text')}
onCursorChange={(page)=>updateCurrentCursorPage(page)}
onViewChange={(page)=>updateCurrentViewPage(page)}
editorTheme={currentEditorTheme}
renderer={brew.renderer}
style={{ height: `calc(100% - ${snippetBarHeight}px)` }}
/>
</>
);
}
if(isStyle()) {
return (
<>
<CodeEditor
key='codeEditor'
ref={codeEditor}
language='css'
tab='brewStyles'
view={view}
value={brew.style ?? DEFAULT_STYLE_TEXT}
onChange={onBrewChange('style')}
editorTheme={currentEditorTheme}
renderer={brew.renderer}
style={{ height: `calc(100% - ${snippetBarHeight}px)` }}
/>
</>
);
}
if(isSnip()) {
if(!brew.snippets) {
brew.snippets = DEFAULT_SNIPPET_TEXT;
}
return (
<>
<CodeEditor
key='codeEditor'
ref={codeEditor}
language='gfm'
tab='brewSnippets'
view={view}
value={brew.snippets}
onChange={onBrewChange('snippets')}
enableFolding={true}
editorTheme={currentEditorTheme}
renderer={brew.renderer}
style={{ height: `calc(100% - 25px)` }}
/>
</>
);
}
if(isMeta()) {
return (
<>
<CodeEditor key='codeEditor' view={view} style={{ display: 'none' }} />
<MetadataEditor
metadata={brew}
themeBundle={themeBundle}
onChange={onBrewChange('metadata')}
reportError={reportError}
userThemes={userThemes}
/>
</>
);
}
};
const redo = ()=>codeEditor.current?.redo();
const historySize = ()=>codeEditor.current?.historySize();
const undo = ()=>codeEditor.current?.undo();
const foldCode = ()=>codeEditor.current?.foldAll();
const unfoldCode = ()=>codeEditor.current?.unfoldAll();
//Called when there are changes to the editor's dimensions
const update = ()=>{};
useImperativeHandle(ref, ()=>({
update,
undo,
redo,
foldCode,
unfoldCode,
historySize,
}));
unfoldCode : function() {
return this.codeEditor.current?.unfoldAll();
},
render : function(){
return ( return (
<div className='editor' ref={this.editor}> <div className='editor' ref={editor}>
<SnippetBar <SnippetBar
brew={this.props.brew} brew={brew}
view={this.state.view} view={view}
onViewChange={this.handleViewChange} onViewChange={handleViewChange}
onInject={this.handleInject} onInject={handleInject}
showEditButtons={this.props.showEditButtons} showEditButtons={showEditButtons}
renderer={this.props.renderer} renderer={renderer}
theme={this.props.brew.theme} theme={brew.theme}
undo={this.undo} undo={undo}
redo={this.redo} redo={redo}
foldCode={this.foldCode} foldCode={foldCode}
unfoldCode={this.unfoldCode} unfoldCode={unfoldCode}
historySize={this.historySize()} historySize={historySize()}
currentEditorTheme={this.state.editorTheme} currentEditorTheme={currentEditorTheme}
updateEditorTheme={this.updateEditorTheme} updateEditorTheme={updateEditorTheme}
themeBundle={this.props.themeBundle} themeBundle={themeBundle}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} cursorPos={codeEditor.current?.getCursorPosition() || {}}
updateBrew={this.props.updateBrew} updateBrew={updateBrew}
/> />
{this.renderEditor()} {renderEditor()}
</div> </div>
); );
} }
}); );
export default Editor; export default Editor;
+26 -30
View File
@@ -16,7 +16,7 @@
"@babel/preset-react": "^8.0.1", "@babel/preset-react": "^8.0.1",
"@babel/runtime": "^8.0.0", "@babel/runtime": "^8.0.0",
"@codemirror/autocomplete": "^6.20.3", "@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.10.3", "@codemirror/commands": "^6.11.0",
"@codemirror/highlight": "^0.19.8", "@codemirror/highlight": "^0.19.8",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-javascript": "^6.2.5",
@@ -25,11 +25,11 @@
"@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.43.8", "@codemirror/view": "^6.43.9",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^20.2.0", "@googleapis/drive": "^21.0.0",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@oddbird/css-anchor-positioning": "^0.10.1", "@oddbird/css-anchor-positioning": "^0.10.2",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"@vitejs/plugin-react": "^6.0.5", "@vitejs/plugin-react": "^6.0.5",
"body-parser": "^2.3.0", "body-parser": "^2.3.0",
@@ -65,7 +65,7 @@
"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.9.2", "mongoose": "^9.9.3",
"nanoid": "6.0.1", "nanoid": "6.0.1",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"node": "^26.7.0", "node": "^26.7.0",
@@ -2633,7 +2633,9 @@
} }
}, },
"node_modules/@codemirror/commands": { "node_modules/@codemirror/commands": {
"version": "6.10.4", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.0.tgz",
"integrity": "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
@@ -3024,7 +3026,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.43.8", "version": "6.43.9",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.9.tgz",
"integrity": "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.7.0", "@codemirror/state": "^6.7.0",
@@ -3420,7 +3424,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@googleapis/drive": { "node_modules/@googleapis/drive": {
"version": "20.2.0", "version": "21.0.0",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-21.0.0.tgz",
"integrity": "sha512-vebQpAqn+FZcmDWh1TEQTcYdkKftcSaHc7kO2ZxrINBb+ZMcFu1UybD3+maeqdLs60HrRDYwyzJ6TgZ7/Vn2Rg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"googleapis-common": "^8.0.0" "googleapis-common": "^8.0.0"
@@ -4273,29 +4279,15 @@
} }
}, },
"node_modules/@oddbird/css-anchor-positioning": { "node_modules/@oddbird/css-anchor-positioning": {
"version": "0.10.1", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@oddbird/css-anchor-positioning/-/css-anchor-positioning-0.10.2.tgz",
"integrity": "sha512-sk+1BpF5V+CfFm0CUbyTRxtwsUjcuP6Nz5tOzC6Bh500gBRdJJZJAeQ/3DYKPd9hGge5cqyVfUNl8zpWJVma/Q==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.7.6", "@floating-ui/dom": "^1.8.0",
"@types/css-tree": "^2.3.11", "@types/css-tree": "^3.2.0",
"css-tree": "^3.2.1", "css-tree": "^3.2.1",
"nanoid": "^5.1.16" "nanoid": "^6.0.1"
}
},
"node_modules/@oddbird/css-anchor-positioning/node_modules/nanoid": {
"version": "5.1.16",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
@@ -4666,7 +4658,9 @@
} }
}, },
"node_modules/@types/css-tree": { "node_modules/@types/css-tree": {
"version": "2.3.11", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-3.2.0.tgz",
"integrity": "sha512-J5KXmk6BFIepOT7280FdFyNs4c5fh0Uee0otjKEvzchrRs38Ii9qminqc4ds0L19X8Zd/rT+brR9jkBthygjWw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/gensync": { "node_modules/@types/gensync": {
@@ -10867,7 +10861,9 @@
} }
}, },
"node_modules/mongoose": { "node_modules/mongoose": {
"version": "9.9.2", "version": "9.9.4",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.9.4.tgz",
"integrity": "sha512-Gc7buf0ExrOZ3t8MD8tVzTek7Qr5d+tEwj4GRFmHCtTZvpaDb38EVsXgCkYacPL9ICAftgS+FGe+dQHLRLHhcw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.1.0", "@standard-schema/spec": "^1.1.0",
+5 -5
View File
@@ -92,7 +92,7 @@
"@babel/preset-react": "^8.0.1", "@babel/preset-react": "^8.0.1",
"@babel/runtime": "^8.0.0", "@babel/runtime": "^8.0.0",
"@codemirror/autocomplete": "^6.20.3", "@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.10.3", "@codemirror/commands": "^6.11.0",
"@codemirror/highlight": "^0.19.8", "@codemirror/highlight": "^0.19.8",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-javascript": "^6.2.5",
@@ -101,11 +101,11 @@
"@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.43.8", "@codemirror/view": "^6.43.9",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^20.2.0", "@googleapis/drive": "^21.0.0",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@oddbird/css-anchor-positioning": "^0.10.1", "@oddbird/css-anchor-positioning": "^0.10.2",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"@vitejs/plugin-react": "^6.0.5", "@vitejs/plugin-react": "^6.0.5",
"body-parser": "^2.3.0", "body-parser": "^2.3.0",
@@ -141,7 +141,7 @@
"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.9.2", "mongoose": "^9.9.3",
"nanoid": "6.0.1", "nanoid": "6.0.1",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"node": "^26.7.0", "node": "^26.7.0",
+26 -20
View File
@@ -2,26 +2,32 @@ import _ from 'lodash';
// Default properties for newly-created brews // Default properties for newly-created brews
const DEFAULT_BREW = { const DEFAULT_BREW = {
title : '', title : '',
text : '', text : '',
style : undefined, style : undefined,
description : '', description : '',
editId : undefined, editId : undefined,
shareId : undefined, shareId : undefined,
createdAt : undefined, createdAt : undefined,
updatedAt : undefined, updatedAt : undefined,
renderer : 'V3', renderer : 'V3',
theme : '5ePHB', theme : '5ePHB',
authors : [], authors : [],
tags : [], tags : [],
lang : 'en', lang : 'en',
thumbnail : '', thumbnail : '',
views : 0, views : 0,
published : false, published : false,
pageCount : 1, pageCount : 1,
gDrive : false, gDrive : false,
trashed : false trashed : false,
bleedSize : { top: '.125in', bottom: '.125in', inner: '.125in', outer: '.125in' },
safetySpace : { top: '.25in', bottom: '.25in', outer: '.25in', inner: '.5in' },
trimSize : { width: '8.5in', height: '11in' },
columns : '2',
columnGutter : '.125in',
license : 'None',
legalAuthors : ''
}; };
// Default values for older brews with missing properties // Default values for older brews with missing properties
// e.g., missing "renderer" is assumed to be "legacy" // e.g., missing "renderer" is assumed to be "legacy"
+8
View File
@@ -218,6 +218,14 @@ const api = {
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']); const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']);
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets; const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined; metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
metadata.bleedSize = { top: brew?.bleedSize?.top, bottom: brew?.bleedSize?.bottom, inner: brew?.bleedSize?.inner, outer: brew?.bleedSize?.outer };
metadata.safetySpace = { top: brew?.safetySpace?.top, bottom: brew?.safetySpace?.bottom, outer: brew?.safetySpace?.outer, inner: brew?.safetySpace?.inner };
metadata.trimSize = { width: brew?.trimSize?.width, height: brew?.trimSize?.height };
metadata.columns = brew?.columns;
metadata.columnGutter = brew?.columnGutter;
metadata.license = brew?.license;
metadata.legalAuthors = brew?.legalAuthors;
text = `\`\`\`metadata\n` + text = `\`\`\`metadata\n` +
`${yaml.dump(metadata)}\n` + `${yaml.dump(metadata)}\n` +
`\`\`\`\n\n` + `\`\`\`\n\n` +
+243 -36
View File
@@ -359,7 +359,27 @@ describe('Tests for api', ()=>{
style : undefined, style : undefined,
trashed : false, trashed : false,
updatedAt : undefined, updatedAt : undefined,
views : 0 views : 0,
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
},
}); });
expect(next).toHaveBeenCalled(); expect(next).toHaveBeenCalled();
expect(api.getId).toHaveBeenCalledWith(req); expect(api.getId).toHaveBeenCalledWith(req);
@@ -450,7 +470,27 @@ describe('Tests for api', ()=>{
tags : ['something', 'fun'], tags : ['something', 'fun'],
renderer : 'v3', renderer : 'v3',
theme : 'phb', theme : 'phb',
googleId : '12345' googleId : '12345',
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
},
}); });
expect(result).toEqual(`\`\`\`metadata expect(result).toEqual(`\`\`\`metadata
@@ -461,6 +501,23 @@ tags:
- fun - fun
renderer: v3 renderer: v3
theme: phb theme: phb
bleedSize:
top: .125in
bottom: .125in
inner: .125in
outer: .125in
safetySpace:
top: .25in
bottom: .25in
outer: .25in
inner: .5in
trimSize:
width: 8.5in
height: 11in
columns: '2'
columnGutter: .125in
license: None
legalAuthors: ''
\`\`\` \`\`\`
@@ -476,7 +533,27 @@ brew`);
tags : ['something', 'fun'], tags : ['something', 'fun'],
renderer : 'v3', renderer : 'v3',
theme : 'phb', theme : 'phb',
googleId : '12345' googleId : '12345',
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
},
}); });
expect(result).toEqual(`\`\`\`metadata expect(result).toEqual(`\`\`\`metadata
@@ -487,6 +564,23 @@ tags:
- fun - fun
renderer: v3 renderer: v3
theme: phb theme: phb
bleedSize:
top: .125in
bottom: .125in
inner: .125in
outer: .125in
safetySpace:
top: .25in
bottom: .25in
outer: .25in
inner: .5in
trimSize:
width: 8.5in
height: 11in
columns: '2'
columnGutter: .125in
license: None
legalAuthors: ''
\`\`\` \`\`\`
@@ -593,16 +687,16 @@ brew`);
expect(res.status).toHaveBeenCalledWith(200); expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({ expect(res.send).toHaveBeenCalledWith({
_id : '1', _id : '1',
authors : ['test user'], authors : ['test user'],
createdAt : undefined, createdAt : undefined,
description : '', description : '',
editId : expect.any(String), editId : expect.any(String),
gDrive : false, gDrive : false,
pageCount : 1, pageCount : 1,
published : false, published : false,
renderer : 'V3', renderer : 'V3',
lang : 'en', lang : 'en',
shareId : expect.any(String), shareId : expect.any(String),
style : undefined, style : undefined,
tags : [], tags : [],
@@ -613,7 +707,27 @@ brew`);
title : 'asdf', title : 'asdf',
trashed : false, trashed : false,
updatedAt : undefined, updatedAt : undefined,
views : 0 views : 0,
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
},
}); });
}); });
@@ -651,28 +765,48 @@ brew`);
expect(google.newGoogleBrew).toHaveBeenCalled(); expect(google.newGoogleBrew).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200); expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({ expect(res.send).toHaveBeenCalledWith({
_id : '1', _id : '1',
authors : ['test user'], authors : ['test user'],
createdAt : undefined, bleedSize: {
description : '', top: '.125in',
editId : expect.any(String), bottom: '.125in',
gDrive : false, inner: '.125in',
pageCount : 1, outer: '.125in',
published : false, },
renderer : 'V3', columns: '2',
lang : 'en', columnGutter: '.125in',
shareId : expect.any(String), createdAt : undefined,
googleId : expect.any(String), description : '',
style : undefined, editId : expect.any(String),
tags : [], gDrive : false,
text : undefined, pageCount : 1,
textBin : undefined, published : false,
theme : '5ePHB', renderer : 'V3',
thumbnail : '', lang : 'en',
title : 'asdf', shareId : expect.any(String),
trashed : false, legalAuthors: '',
updatedAt : undefined, license: 'None',
views : 0 safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
},
googleId : expect.any(String),
style : undefined,
tags : [],
text : undefined,
textBin : undefined,
theme : '5ePHB',
thumbnail : '',
title : 'asdf',
trashed : false,
trimSize: {
width: '8.5in',
height: '11in',
},
updatedAt : undefined,
views : 0
}); });
}); });
}); });
@@ -1164,6 +1298,79 @@ brew`);
// Text // Text
expect(testBrew.text).toEqual('text\n'); expect(testBrew.text).toEqual('text\n');
}); });
it('extended metadata', async ()=>{
const testBrew = {
text : '```metadata\n' +
'title: title\n' +
'description: description\n' +
'tags: [ \'tag a\' , \'tag b\' ]\n' +
'renderer: legacy\n' +
'theme: 5ePHB\n' +
'lang: en\n' +
'bleedSize:\n' +
' top: 1.5in\n' +
' bottom: 1.5in\n' +
' outer: 1.5in\n' +
' inner: 1.5in\n' +
'safetySpace:\n' +
' top: 1.25in\n' +
' bottom: 1.25in\n' +
' outer: 1.25in\n' +
' inner: 1.5in\n' +
'trimSize:\n' +
' width: 18.5in\n' +
' height: 111in\n' +
'columns: 12\n' +
'columnGutter: 1.125in\n' +
'license: AELF\n' +
'legalAuthors: Tom Bombadil\n' +
'\n' +
'```\n' +
'\n' +
'```css\n' +
'style\n' +
'style\n' +
'style\n' +
'```\n' +
'\n' +
'text\n'
};
splitTextStyleAndMetadata(testBrew);
// Metadata
expect(testBrew.title).toEqual('title');
expect(testBrew.description).toEqual('description');
expect(testBrew.renderer).toEqual('legacy');
expect(testBrew.theme).toEqual('5ePHB');
expect(testBrew.lang).toEqual('en');
// Paper Specfications
expect(testBrew.bleedSize.top).toEqual('1.5in');
expect(testBrew.bleedSize.bottom).toEqual('1.5in');
expect(testBrew.bleedSize.inner).toEqual('1.5in');
expect(testBrew.bleedSize.outer).toEqual('1.5in');
expect(testBrew.safetySpace.top).toEqual('1.25in');
expect(testBrew.safetySpace.bottom).toEqual('1.25in');
expect(testBrew.safetySpace.inner).toEqual('1.5in');
expect(testBrew.safetySpace.outer).toEqual('1.25in');
expect(testBrew.trimSize.width).toEqual('18.5in');
expect(testBrew.trimSize.height).toEqual('111in');
expect(testBrew.columns).toEqual(12);
expect(testBrew.columnGutter).toEqual('1.125in');
// Extended Metadata
expect(testBrew.license).toEqual('AELF');
expect(testBrew.legalAuthors).toEqual('Tom Bombadil');
// Style
expect(testBrew.style).toEqual('style\nstyle\nstyle\n');
// Text
expect(testBrew.text).toEqual('text\n');
});
}); });
}); });
+9
View File
@@ -93,6 +93,15 @@ const splitTextStyleAndMetadata = (brew)=>{
const metadata = yaml.load(metadataSection); const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'renderer', 'theme', 'lang'])); Object.assign(brew, _.pick(metadata, ['title', 'description', 'renderer', 'theme', 'lang']));
brew.snippets = yamlSnippetsToText(_.pick(metadata, ['snippets']).snippets || ''); brew.snippets = yamlSnippetsToText(_.pick(metadata, ['snippets']).snippets || '');
brew.bleedSize = { ...metadata.bleedSize };
brew.safetySpace = { ...metadata.safetySpace };
brew.trimSize = { ...metadata.trimSize };
brew.columns = metadata?.columns;
brew.columnGutter = metadata?.columnGutter;
brew.license = metadata?.license;
brew.legalAuthors = metadata.legalAuthors;
brew.text = brew.text.slice(index + 6); brew.text = brew.text.slice(index + 6);
} }
if(brew.text.startsWith('```css')) { if(brew.text.startsWith('```css')) {