0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-30 15:22:41 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into sort-in-vault

This commit is contained in:
Víctor Losada Hernández
2024-09-20 20:54:14 +02:00
34 changed files with 1649 additions and 1033 deletions

View File

@@ -1,7 +1,7 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less'); require('./brewRenderer.less');
const React = require('react'); const React = require('react');
const { useState, useRef, useEffect } = React; const { useState, useRef, useEffect, useCallback } = React;
const _ = require('lodash'); const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
@@ -49,23 +49,25 @@ let rawPages = [];
const BrewRenderer = (props)=>{ const BrewRenderer = (props)=>{
props = { props = {
text : '', text : '',
style : '', style : '',
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB', theme : '5ePHB',
lang : '', lang : '',
errors : [], errors : [],
currentEditorPage : 0, currentEditorCursorPageNum : 0,
themeBundle : {}, currentEditorViewPageNum : 0,
currentBrewRendererPageNum : 0,
themeBundle : {},
onPageChange : ()=>{},
...props ...props
}; };
const [state, setState] = useState({ const [state, setState] = useState({
height : PAGE_HEIGHT, height : PAGE_HEIGHT,
isMounted : false, isMounted : false,
visibility : 'hidden', visibility : 'hidden',
zoom : 100, zoom : 100
currentPageNumber : 1,
}); });
const mainRef = useRef(null); const mainRef = useRef(null);
@@ -87,25 +89,22 @@ const BrewRenderer = (props)=>{
})); }));
}; };
const getCurrentPage = (e)=>{ const updateCurrentPage = useCallback(_.throttle((e)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target; const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight; const totalScrollableHeight = scrollHeight - clientHeight;
const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length); const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
setState((prevState)=>({ props.onPageChange(currentPageNumber);
...prevState, }, 200), []);
currentPageNumber : currentPageNumber || 1
}));
};
const isInView = (index)=>{ const isInView = (index)=>{
if(!state.isMounted) if(!state.isMounted)
return false; return false;
if(index == props.currentEditorPage) //Already rendered before this step if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step
return false; return false;
if(Math.abs(index - state.currentPageNumber) <= 3) if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3)
return true; return true;
return false; return false;
@@ -142,7 +141,7 @@ const BrewRenderer = (props)=>{
renderedPages.length = 0; renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first // Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage); 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'){ if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
@@ -192,7 +191,7 @@ const BrewRenderer = (props)=>{
<> <>
{/*render dummy page while iFrame is mounting.*/} {/*render dummy page while iFrame is mounting.*/}
{!state.isMounted {!state.isMounted
? <div className='brewRenderer' onScroll={getCurrentPage}> ? <div className='brewRenderer' onScroll={updateCurrentPage}>
<div className='pages'> <div className='pages'>
{renderDummyPage(1)} {renderDummyPage(1)}
</div> </div>
@@ -205,7 +204,7 @@ const BrewRenderer = (props)=>{
<NotificationPopup /> <NotificationPopup />
</div> </div>
<ToolBar onZoomChange={handleZoom} currentPage={state.currentPageNumber} totalPages={rawPages.length}/> <ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length}/>
{/*render in iFrame so broken code doesn't crash the site.*/} {/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT} <Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
@@ -214,7 +213,7 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={getCurrentPage} onScroll={updateCurrentPage}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={{ height: state.height }}> style={{ height: state.height }}>

View File

@@ -17,7 +17,7 @@ const NotificationPopup = ()=>{
<ul> <ul>
<li key='Vault'> <li key='Vault'>
<em>Search brews with our new page!</em><br /> <em>Search brews with our new page!</em><br />
We have been working very hard in making this possible, now you can share your work and look at it in the new <a href="/vault">Vault</a> page! We have been working very hard in making this possible, now you can share your work and look at it in the new <a href='/vault'>Vault</a> page!
All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer. All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
More features will be coming. More features will be coming.

View File

@@ -11,6 +11,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100); const [zoomLevel, setZoomLevel] = useState(100);
const [pageNum, setPageNum] = useState(currentPage); const [pageNum, setPageNum] = useState(currentPage);
const [toolsVisible, setToolsVisible] = useState(true);
useEffect(()=>{ useEffect(()=>{
onZoomChange(zoomLevel); onZoomChange(zoomLevel);
@@ -55,7 +56,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
} else if(mode == 'fit'){ } else if(mode == 'fit'){
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it. // find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
const minDimRatio = [...pages].reduce((minRatio, page) => Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity); const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
desiredZoom = minDimRatio * 100; desiredZoom = minDimRatio * 100;
} }
@@ -67,7 +68,8 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
}; };
return ( return (
<div className='toolBar'> <div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}>
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/} {/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group'> <div className='group'>
<button <button

View File

@@ -15,6 +15,10 @@
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
color : #CCCCCC; color : #CCCCCC;
background-color : #555555; background-color : #555555;
& > *:not(.toggleButton) {
opacity: 1;
transition: all .2s ease;
}
.group { .group {
box-sizing : border-box; box-sizing : border-box;
@@ -100,4 +104,25 @@
font-size:1.2em; font-size:1.2em;
} }
} }
&.hidden {
width: 32px;
transition: all .3s ease;
flex-wrap:nowrap;
overflow: hidden;
background-color: unset;
opacity: .5;
& > *:not(.toggleButton) {
opacity: 0;
transition: all .2s ease;
}
}
}
button.toggleButton {
z-index : 5;
position:absolute;
left: 0;
width: 32px;
min-width: unset;
} }

View File

@@ -1,9 +1,8 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
require('./editor.less'); require('./editor.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const Markdown = require('../../../shared/naturalcrit/markdown.js'); const Markdown = require('../../../shared/naturalcrit/markdown.js');
@@ -22,6 +21,7 @@ const DEFAULT_STYLE_TEXT = dedent`
color: black; color: black;
}`; }`;
let isJumping = false;
const Editor = createClass({ const Editor = createClass({
displayName : 'Editor', displayName : 'Editor',
@@ -37,8 +37,15 @@ const Editor = createClass({
onMetaChange : ()=>{}, onMetaChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
onCursorPageChange : ()=>{},
onViewPageChange : ()=>{},
editorTheme : 'default', editorTheme : 'default',
renderer : 'legacy' renderer : 'legacy',
currentEditorCursorPageNum : 1,
currentEditorViewPageNum : 1,
currentBrewRendererPageNum : 1,
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -56,12 +63,16 @@ const Editor = createClass({
isMeta : function() {return this.state.view == 'meta';}, isMeta : function() {return this.state.view == 'meta';},
componentDidMount : function() { componentDidMount : function() {
this.updateEditorSize(); this.updateEditorSize();
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) { if(editorTheme) {
this.setState({ this.setState({
@@ -75,28 +86,37 @@ const Editor = createClass({
}, },
componentDidUpdate : function(prevProps, prevState, snapshot) { componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew) { if(prevProps.moveBrew !== this.props.moveBrew)
this.brewJump(); this.brewJump();
};
if(prevProps.moveSource !== this.props.moveSource) { if(prevProps.moveSource !== this.props.moveSource)
this.sourceJump(); 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);
}
}
}, },
handleControlKeys : function(e){ handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey)) return; if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
const LEFTARROW_KEY = 37; const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39; const RIGHTARROW_KEY = 39;
if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump(); if(e.keyCode == RIGHTARROW_KEY) this.brewJump();
if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump(); if(e.keyCode == LEFTARROW_KEY) this.sourceJump();
if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) { if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} }
}, },
updateEditorSize : function() { updateEditorSize : function() {
if(this.codeEditor.current) { if(this.codeEditor.current) {
let paneHeight = this.editor.current.parentNode.clientHeight; let paneHeight = this.editor.current.parentNode.clientHeight;
@@ -105,6 +125,20 @@ const Editor = createClass({
} }
}, },
updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onCursorPageChange(currentPage);
},
updateCurrentViewPage : function(topScrollLine) {
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onViewPageChange(currentPage);
},
handleInject : function(injectText){ handleInject : function(injectText){
this.codeEditor.current?.injectText(injectText, false); this.codeEditor.current?.injectText(injectText, false);
}, },
@@ -119,18 +153,6 @@ const Editor = createClass({
}); //TODO: not sure if updateeditorsize needed }); //TODO: not sure if updateeditorsize needed
}, },
getCurrentPage : function(){
const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1);
return _.reduce(lines, (r, line)=>{
if(
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
||
(this.props.renderer == 'V3' && line.match(/^\\page$/))
) r++;
return r;
}, 1);
},
highlightCustomMarkdown : function(){ highlightCustomMarkdown : function(){
if(!this.codeEditor.current) return; if(!this.codeEditor.current) return;
if(this.state.view === 'text') { if(this.state.view === 'text') {
@@ -145,7 +167,7 @@ const Editor = createClass({
// Record details of folded sections // Record details of folded sections
if(mark.__isFold) { if(mark.__isFold) {
const fold = mark.find(); const fold = mark.find();
foldLines.push({from: fold.from?.line, to: fold.to?.line}); foldLines.push({ from: fold.from?.line, to: fold.to?.line });
} }
return !mark.__isFold; return !mark.__isFold;
}); //Don't undo code folding }); //Don't undo code folding
@@ -163,7 +185,7 @@ const Editor = createClass({
// Don't process lines inside folded text // Don't process lines inside folded text
// If the current lineNumber is inside any folded marks, skip line styling // If the current lineNumber is inside any folded marks, skip line styling
if (foldLines.some(fold => lineNumber >= fold.from && lineNumber <= fold.to)) if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
return; return;
// Styling for \page breaks // Styling for \page breaks
@@ -189,7 +211,7 @@ const Editor = createClass({
// definition lists // definition lists
if(line.includes('::')){ if(line.includes('::')){
if(/^:*$/.test(line) == true){ return }; if(/^:*$/.test(line) == true){ return; };
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error. const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
let match; let match;
while ((match = regex.exec(line)) != null){ while ((match = regex.exec(line)) != null){
@@ -197,10 +219,10 @@ const Editor = createClass({
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
const ddIndex = match.indices[2][0]; const ddIndex = match.indices[2][0];
let colons = /::/g; const colons = /::/g;
let colonMatches = colons.exec(match[2]); const colonMatches = colons.exec(match[2]);
if(colonMatches !== null){ if(colonMatches !== null){
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} ) codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
} }
} }
} }
@@ -214,8 +236,8 @@ const Editor = createClass({
while (startIndex >= 0) { while (startIndex >= 0) {
superRegex.lastIndex = subRegex.lastIndex = startIndex; superRegex.lastIndex = subRegex.lastIndex = startIndex;
let isSuper = false; let isSuper = false;
let match = subRegex.exec(line) || superRegex.exec(line); const match = subRegex.exec(line) || superRegex.exec(line);
if (match) { if(match) {
isSuper = !subRegex.lastIndex; isSuper = !subRegex.lastIndex;
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' }); codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
} }
@@ -265,18 +287,18 @@ const Editor = createClass({
while (startIndex >= 0) { while (startIndex >= 0) {
emojiRegex.lastIndex = startIndex; emojiRegex.lastIndex = startIndex;
let match = emojiRegex.exec(line); const match = emojiRegex.exec(line);
if (match) { if(match) {
let tokens = Markdown.marked.lexer(match[0]); let tokens = Markdown.marked.lexer(match[0]);
tokens = tokens[0].tokens.filter(t => t.type == 'emoji') tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
if (!tokens.length) if(!tokens.length)
return; return;
let startPos = { line: lineNumber, ch: match.index }; const startPos = { line: lineNumber, ch: match.index };
let endPos = { line: lineNumber, ch: match.index + match[0].length }; const endPos = { line: lineNumber, ch: match.index + match[0].length };
// Iterate over conflicting marks and clear them // Iterate over conflicting marks and clear them
var marks = codeMirror.findMarks(startPos, endPos); const marks = codeMirror.findMarks(startPos, endPos);
marks.forEach(function(marker) { marks.forEach(function(marker) {
if(!marker.__isFold) marker.clear(); if(!marker.__isFold) marker.clear();
}); });
@@ -291,75 +313,93 @@ const Editor = createClass({
} }
}, },
brewJump : function(targetPage=this.getCurrentPage()){ brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window) return; if(!window || isJumping)
// console.log(`Scroll to: p${targetPage}`); return;
// Get current brewRenderer scroll position and calculate target position
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const interimPos = targetPos >= 0 ? -30 : 30;
const bounceDelay = 100; const checkIfScrollComplete = ()=>{
const scrollDelay = 500; let scrollingTimeout;
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
if(!this.throttleBrewMove) { scrollingTimeout = setTimeout(()=>{
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{ isJumping = false;
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' }); brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
setTimeout(()=>{ }, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
}; };
this.throttleBrewMove(currentPos, interimPos, targetPos);
// const hashPage = (page != 1) ? `p${page}` : ''; isJumping = true;
// window.location.hash = hashPage; 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(targetLine=null){ sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
if(this.isText()) { if(!this.isText || isJumping)
if(targetLine == null) { return;
targetLine = 0;
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page'); const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height; const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
let currentPage = 1; let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
for (const page of pageCollection) { let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
currentPage = parseInt(page.id.slice(1)) || 1; const checkIfScrollComplete = ()=>{
break; let scrollingTimeout;
} clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
};
isJumping = true;
checkIfScrollComplete();
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
if(smooth) {
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
} }
}, 10);
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/; } else {
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit); this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
const textPosition = textString.length; this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0; this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
}
}, 10);
}
} }
}, },
@@ -460,7 +500,9 @@ const Editor = createClass({
currentEditorTheme={this.state.editorTheme} currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme} updateEditorTheme={this.updateEditorTheme}
snippetBundle={this.props.snippetBundle} snippetBundle={this.props.snippetBundle}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} /> cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
updateBrew={this.props.updateBrew}
/>
{this.renderEditor()} {this.renderEditor()}
</div> </div>

View File

@@ -304,17 +304,14 @@ const MetadataEditor = createClass({
onChange={(e)=>this.handleRenderer('V3', e)} /> onChange={(e)=>this.handleRenderer('V3', e)} />
V3 V3
</label> </label>
<small><a href='/legacy' target='_blank' rel='noopener noreferrer'>Click here to see the demo page for the old Legacy renderer!</a></small>
<a href='/legacy' target='_blank' rel='noopener noreferrer'>
Click here to see the demo page for the old Legacy renderer!
</a>
</div> </div>
</div>; </div>;
}, },
render : function(){ render : function(){
return <div className='metadataEditor'> return <div className='metadataEditor'>
<h1 className='sectionHead'>Brew</h1> <h1>Properties Editor</h1>
<div className='field title'> <div className='field title'>
<label>title</label> <label>title</label>
@@ -362,9 +359,7 @@ const MetadataEditor = createClass({
{this.renderRenderOptions()} {this.renderRenderOptions()}
<hr/> <h2>Authors</h2>
<h1 className='sectionHead'>Authors</h1>
{this.renderAuthors()} {this.renderAuthors()}
@@ -375,15 +370,13 @@ const MetadataEditor = createClass({
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
<hr/> <h2>Privacy</h2>
<h1 className='sectionHead'>Privacy</h1>
<div className='field publish'> <div className='field publish'>
<label>publish</label> <label>publish</label>
<div className='value'> <div className='value'>
{this.renderPublish()} {this.renderPublish()}
<small>Published homebrews will be publicly viewable and searchable (eventually...)</small> <small>Published brews are searchable in <a href='/vault'>the Vault</a> and visible on your user page. Unpublished brews are not indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a brew any time.</small>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
@import 'naturalcrit/styles/colors.less'; @import 'naturalcrit/styles/colors.less';
.metadataEditor { .metadataEditor {
position : absolute; position : absolute;
z-index : 5; z-index : 5;
@@ -9,12 +10,19 @@
padding : 25px; padding : 25px;
overflow-y : auto; overflow-y : auto;
background-color : #999999; background-color : #999999;
font-size : 13px;
.sectionHead { h1 {
margin: 0 0 40px;
font-weight: bold;
text-transform: uppercase;
}
h2 {
margin : 20px 0; margin : 20px 0;
font-weight : 1000; font-weight : bold;
border-bottom: 2px solid gray;
&:first-of-type { margin-top : 0; } color: #555;
} }
& > div { margin-bottom : 10px; } & > div { margin-bottom : 10px; }
@@ -43,15 +51,21 @@
min-width : 200px; min-width : 200px;
& > label { & > label {
width : 80px; width : 80px;
font-size : 11px;
font-weight : 800; font-weight : 800;
line-height : 1.8em; line-height : 1.8em;
text-transform : uppercase; text-transform : uppercase;
font-size: .9em;
} }
& > .value { & > .value {
flex : 1 1 auto; flex : 1 1 auto;
width : 50px; width : 50px;
&:invalid { background : #FFB9B9; } &:invalid { background : #FFB9B9; }
small {
display : block;
font-size : 0.9em;
font-style : italic;
line-height : 1.4em;
}
} }
input[type='text'], textarea { input[type='text'], textarea {
border : 1px solid gray; border : 1px solid gray;
@@ -78,7 +92,6 @@
textarea.value { textarea.value {
height : auto; height : auto;
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
font-size : 0.8em;
resize : none; resize : none;
} }
} }
@@ -87,12 +100,6 @@
z-index : 200; z-index : 200;
max-width : 150px; max-width : 150px;
} }
small {
display : inline-block;
font-size : 0.6em;
font-style : italic;
line-height : 1.4em;
}
} }
@@ -113,18 +120,13 @@
display : inline-flex; display : inline-flex;
align-items : center; align-items : center;
margin-right : 15px; margin-right : 15px;
font-size : 0.7em; font-size : 0.9em;
font-weight : 800; font-weight : 800;
white-space : nowrap; white-space : nowrap;
vertical-align : middle; vertical-align : middle;
cursor : pointer; cursor : pointer;
user-select : none; user-select : none;
} }
a {
display : inline-flex;
font-size : 0.7em;
font-weight : 800;
}
input { input {
margin : 3px; margin : 3px;
vertical-align : middle; vertical-align : middle;
@@ -149,12 +151,10 @@
} }
} }
.authors.field .value { .authors.field .value {
font-size : 0.8em;
line-height : 1.5em; line-height : 1.5em;
} }
.themes.field { .themes.field {
font-size : 13.33px;
.navDropdownContainer { .navDropdownContainer {
position : relative; position : relative;
z-index : 100; z-index : 100;
@@ -165,9 +165,9 @@
background-color : darkgray; background-color : darkgray;
} }
& > div:first-child { & > div:first-child {
padding : 6px 3px; padding : 3px 3px;
background-color : inherit; background-color : inherit;
border : 2px solid rgb(118,118,118); border : 1px solid gray;
i { float : right; } i { float : right; }
&:hover { &:hover {
color : white; color : white;
@@ -240,6 +240,7 @@
} }
} }
} }
.field .list { .field .list {
display : flex; display : flex;
flex : 1 0; flex : 1 0;
@@ -277,8 +278,7 @@
background-color : #DDDDDD; background-color : #DDDDDD;
border-radius : 0.5em; border-radius : 0.5em;
.icon { .icon { #groupedIcon; }
#groupedIcon; }
} }
.input-group { .input-group {
@@ -294,16 +294,29 @@
height : 100%; height : 100%;
} }
.invalid:focus { background-color : pink; } .input-group {
height : ~'calc(.9em + 4px + .6em)';
.icon { input { border-radius : 0.5em 0 0 0.5em; }
#groupedIcon;
top : -0.54em;
right : 1px;
height : 97%;
font-size : 0.8em;
i { font-size : 1.125em; } input:last-child { border-radius : 0.5em; }
.value {
width : 7.5vw;
min-width : 75px;
height : 100%;
}
.invalid:focus { background-color : pink; }
.icon {
#groupedIcon;
top : -0.54em;
right : 1px;
height : 97%;
i { font-size : 1.125em; }
}
} }
} }
} }

View File

@@ -5,6 +5,8 @@ const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
//Import all themes //Import all themes
const ThemeSnippets = {}; const ThemeSnippets = {};
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js'); ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
@@ -38,7 +40,8 @@ const Snippetbar = createClass({
unfoldCode : ()=>{}, unfoldCode : ()=>{},
updateEditorTheme : ()=>{}, updateEditorTheme : ()=>{},
cursorPos : {}, cursorPos : {},
snippetBundle : [] snippetBundle : [],
updateBrew : ()=>{}
}; };
}, },
@@ -46,7 +49,8 @@ const Snippetbar = createClass({
return { return {
renderer : this.props.renderer, renderer : this.props.renderer,
themeSelector : false, themeSelector : false,
snippets : [] snippets : [],
historyExists : false
}; };
}, },
@@ -59,18 +63,23 @@ const Snippetbar = createClass({
componentDidUpdate : async function(prevProps) { componentDidUpdate : async function(prevProps) {
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) { if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
const snippets = this.compileSnippets();
this.setState({ this.setState({
snippets : snippets snippets : this.compileSnippets()
}); });
} };
},
if(historyExists(this.props.brew) != this.state.historyExists){
this.setState({
historyExists : !this.state.historyExists
});
};
},
mergeCustomizer : function(oldValue, newValue, key) { mergeCustomizer : function(oldValue, newValue, key) {
if(key == 'snippets') { if(key == 'snippets') {
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
return result.filter(snip => snip.gen || snip.subsnippets); return result.filter((snip)=>snip.gen || snip.subsnippets);
} }
}, },
@@ -138,6 +147,36 @@ const Snippetbar = createClass({
}); });
}, },
replaceContent : function(item){
return this.props.updateBrew(item);
},
renderHistoryItems : function() {
const historyItems = getHistoryItems(this.props.brew);
return <div className='dropdown'>
{_.map(historyItems, (item, index)=>{
if(!item.savedAt) return;
const saveTime = new Date(item.savedAt);
const diffMs = new Date() - saveTime;
const diffSecs = Math.floor(diffMs / 1000);
let diffString = `about ${diffSecs} seconds ago`;
if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`;
if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`;
if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`;
if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`;
return <div className='snippet' key={index} onClick={()=>{this.replaceContent(item);}} >
<i className={`fas fa-${index+1}`} />
<span className='name' title={saveTime.toISOString()}>v{item.version} : {diffString}</span>
</div>;
})}
</div>;
},
renderEditorButtons : function(){ renderEditorButtons : function(){
if(!this.props.showEditButtons) return; if(!this.props.showEditButtons) return;
@@ -158,6 +197,10 @@ const Snippetbar = createClass({
} }
return <div className='editors'> return <div className='editors'>
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} >
<i className='fas fa-clock-rotate-left' />
{this.state.historyExists && this.renderHistoryItems() }
</div>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`} <div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
onClick={this.props.undo} > onClick={this.props.undo} >
<i className='fas fa-undo' /> <i className='fas fa-undo' />

View File

@@ -53,6 +53,21 @@
font-size : 0.75em; font-size : 0.75em;
color : inherit; color : inherit;
} }
&.history {
.tooltipLeft('History');
font-size : 0.75em;
color : grey;
position : relative;
&.active {
color : inherit;
}
&>.dropdown{
right : -1px;
&>.snippet{
padding-right : 10px;
}
}
}
&.editorTheme { &.editorTheme {
.tooltipLeft('Editor Themes'); .tooltipLeft('Editor Themes');
font-size : 0.75em; font-size : 0.75em;

View File

@@ -128,7 +128,7 @@ const StringArrayEditor = createClass({
return <div className='field'> return <div className='field'>
<label>{this.props.label}</label> <label>{this.props.label}</label>
<div style={{ flex: '1 0' }}> <div style={{ flex: '1 0' }} className='value'>
<div className='list'> <div className='list'>
{valueElements} {valueElements}
<div className='input-group'> <div className='input-group'>

View File

@@ -111,7 +111,7 @@ const ErrorNavItem = createClass({
Looks like there was a problem retreiving Looks like there was a problem retreiving
the theme, or a theme that it inherits, the theme, or a theme that it inherits,
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}> for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
{response.body.brewId}</a> still exists! {response.body.brewId}</a> still exists!
</div> </div>
</Nav.item>; </Nav.item>;
} }

View File

@@ -35,6 +35,11 @@
display : flex; display : flex;
align-items : center; align-items : center;
&:last-child .navItem { border-left : 1px solid #666666; } &:last-child .navItem { border-left : 1px solid #666666; }
&:has(.brewTitle) {
flex-grow : 1;
min-width : 300px;
}
} }
// "NaturalCrit" logo // "NaturalCrit" logo
.navLogo { .navLogo {
@@ -69,6 +74,10 @@
.navItem { .navItem {
#backgroundColorsHover; #backgroundColorsHover;
.animate(background-color); .animate(background-color);
display : flex;
align-items : center;
justify-content : center;
height : 100%;
padding : 8px 12px; padding : 8px 12px;
font-size : 10px; font-size : 10px;
font-weight : 800; font-weight : 800;
@@ -94,39 +103,20 @@
animation-duration : 2s; animation-duration : 2s;
} }
} }
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
padding : 2px 12px;
input {
width : 250px;
padding : 2px;
margin : 0;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
background-color : transparent;
border : 1px solid @blue;
outline : none;
}
.charCount {
display : inline-block;
margin-left : 8px;
color : #666666;
text-align : right;
vertical-align : bottom;
&.max { color : @red; }
}
}
&.brewTitle { &.brewTitle {
flex-grow : 1; display : block;
width : 100%;
overflow : hidden;
font-size : 12px; font-size : 12px;
font-weight : 800; font-weight : 800;
color : white; color : white;
text-align : center; text-align : center;
text-overflow : ellipsis;
text-transform : initial; text-transform : initial;
white-space : nowrap;
background-color : transparent; background-color : transparent;
} }
// "The Homebrewery" logo // "The Homebrewery" logo
&.homebrewLogo { &.homebrewLogo {
.animate(color); .animate(color);
@@ -240,23 +230,25 @@
} }
.navDropdownContainer { .navDropdownContainer {
position : relative; position : relative;
height : 100%;
.navDropdown { .navDropdown {
position: absolute; position : absolute;
top: 28px; //top: 28px;
right: 0px; right : 0px;
z-index: 10000; z-index : 10000;
width: max-content; display : flex;
min-width:100%; flex-direction : column;
max-height: calc(100vh - 28px); align-items : flex-end;
overflow: hidden auto; width : max-content;
display: flex; min-width : 100%;
flex-direction: column; max-height : calc(100vh - 28px);
align-items: flex-end; overflow : hidden auto;
.navItem { .navItem {
position : relative; position : relative;
display : flex; display : flex;
justify-content : space-between;
align-items : center; align-items : center;
justify-content : space-between;
width : 100%; width : 100%;
border : 1px solid #888888; border : 1px solid #888888;
border-bottom : 0; border-bottom : 0;
@@ -278,10 +270,10 @@
overflow : hidden auto; overflow : hidden auto;
color : white; color : white;
text-decoration : none; text-decoration : none;
background-color : #333333;
border-top : 1px solid #888888;
scrollbar-color : #666666 #333333; scrollbar-color : #666666 #333333;
scrollbar-width : thin; scrollbar-width : thin;
background-color : #333333;
border-top : 1px solid #888888;
.clear { .clear {
position : absolute; position : absolute;
top : 50%; top : 50%;

View File

@@ -1,8 +1,9 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
require('./editPage.less'); require('./editPage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const createClass = require('create-react-class');
const request = require('../../utils/request-middleware.js'); const request = require('../../utils/request-middleware.js');
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
@@ -27,6 +28,8 @@ const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
const googleDriveIcon = require('../../googleDrive.svg'); const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 3000; const SAVE_TIMEOUT = 3000;
@@ -41,22 +44,24 @@ const EditPage = createClass({
getInitialState : function() { getInitialState : function() {
return { return {
brew : this.props.brew, brew : this.props.brew,
isSaving : false, isSaving : false,
isPending : false, isPending : false,
alertTrashedGoogleBrew : this.props.brew.trashed, alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false, alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false, saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false, confirmGoogleTransfer : false,
error : null, error : null,
htmlErrors : Markdown.validate(this.props.brew.text), htmlErrors : Markdown.validate(this.props.brew.text),
url : '', url : '',
autoSave : true, autoSave : true,
autoSaveWarning : false, autoSaveWarning : false,
unsavedTime : new Date(), unsavedTime : new Date(),
currentEditorPage : 0, currentEditorViewPageNum : 1,
displayLockMessage : this.props.brew.lock || false, currentEditorCursorPageNum : 1,
themeBundle : {} currentBrewRendererPageNum : 1,
displayLockMessage : this.props.brew.lock || false,
themeBundle : {}
}; };
}, },
@@ -113,16 +118,27 @@ const EditPage = createClass({
this.editor.current.update(); this.editor.current.update();
}, },
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){ handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback //If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
isPending : true, isPending : true,
htmlErrors : htmlErrors, htmlErrors : htmlErrors,
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
}), ()=>{if(this.state.autoSave) this.trySave();}); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
@@ -150,6 +166,16 @@ const EditPage = createClass({
return !_.isEqual(this.state.brew, this.savedBrew); return !_.isEqual(this.state.brew, this.savedBrew);
}, },
updateBrew : function(newData){
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style,
text : newData.text
}
}));
},
trySave : function(immediate=false){ trySave : function(immediate=false){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){ if(this.hasChanges()){
@@ -202,6 +228,9 @@ const EditPage = createClass({
htmlErrors : Markdown.validate(prevState.brew.text) htmlErrors : Markdown.validate(prevState.brew.text)
})); }));
updateHistory(this.state.brew);
versionHistoryGarbageCollection();
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const brew = this.state.brew; const brew = this.state.brew;
@@ -413,6 +442,12 @@ const EditPage = createClass({
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
userThemes={this.props.userThemes} userThemes={this.props.userThemes}
snippetBundle={this.state.themeBundle.snippets} snippetBundle={this.state.themeBundle.snippets}
updateBrew={this.updateBrew}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
@@ -422,7 +457,10 @@ const EditPage = createClass({
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors} errors={this.state.htmlErrors}
lang={this.state.brew.lang} lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>

View File

@@ -1,7 +1,6 @@
require('./homePage.less'); require('./homePage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require('../../utils/request-middleware.js'); const request = require('../../utils/request-middleware.js');
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
@@ -32,11 +31,13 @@ const HomePage = createClass({
}, },
getInitialState : function() { getInitialState : function() {
return { return {
brew : this.props.brew, brew : this.props.brew,
welcomeText : this.props.brew.text, welcomeText : this.props.brew.text,
error : undefined, error : undefined,
currentEditorPage : 0, currentEditorViewPageNum : 1,
themeBundle : {} currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {}
}; };
}, },
@@ -61,10 +62,22 @@ const HomePage = createClass({
handleSplitMove : function(){ handleSplitMove : function(){
this.editor.current.update(); this.editor.current.update();
}, },
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){ handleTextChange : function(text){
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
})); }));
}, },
renderNavbar : function(){ renderNavbar : function(){
@@ -97,12 +110,20 @@ const HomePage = createClass({
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
showEditButtons={false} showEditButtons={false}
snippetBundle={this.state.themeBundle.snippets} snippetBundle={this.state.themeBundle.snippets}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
style={this.state.brew.style} style={this.state.brew.style}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
currentEditorPage={this.state.currentEditorPage} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
/> />
</SplitPane> </SplitPane>

View File

@@ -39,13 +39,15 @@ const NewPage = createClass({
const brew = this.props.brew; const brew = this.props.brew;
return { return {
brew : brew, brew : brew,
isSaving : false, isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false), saveGoogle : (global.account && global.account.googleId ? true : false),
error : null, error : null,
htmlErrors : Markdown.validate(brew.text), htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0, currentEditorViewPageNum : 1,
themeBundle : {} currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {}
}; };
}, },
@@ -108,15 +110,26 @@ const NewPage = createClass({
this.editor.current.update(); this.editor.current.update();
}, },
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){ handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback //If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors, htmlErrors : htmlErrors,
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
})); }));
localStorage.setItem(BREWKEY, text); localStorage.setItem(BREWKEY, text);
}, },
@@ -221,6 +234,11 @@ const NewPage = createClass({
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
userThemes={this.props.userThemes} userThemes={this.props.userThemes}
snippetBundle={this.state.themeBundle.snippets} snippetBundle={this.state.themeBundle.snippets}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
@@ -230,7 +248,10 @@ const NewPage = createClass({
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors} errors={this.state.htmlErrors}
lang={this.state.brew.lang} lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>

View File

@@ -0,0 +1,116 @@
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
export const HISTORY_SLOTS = 5;
// History values in minutes
const DEFAULT_HISTORY_SAVE_DELAYS = {
'0' : 0,
'1' : 2,
'2' : 10,
'3' : 60,
'4' : 12 * 60,
'5' : 2 * 24 * 60
};
const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS;
const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY;
function getKeyBySlot(brew, slot){
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
};
function getVersionBySlot(brew, slot){
// Read stored brew data
// - If it exists, parse data to object
// - If it doesn't exist, pass default object
const key = getKeyBySlot(brew, slot);
const storedVersion = localStorage.getItem(key);
const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
return output;
};
function updateStoredBrew(brew, slot = 0) {
const archiveBrew = {
title : brew.title,
text : brew.text,
style : brew.style,
version : brew.version,
shareId : brew.shareId,
savedAt : brew?.savedAt || new Date(),
expireAt : new Date()
};
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
const key = getKeyBySlot(brew, slot);
localStorage.setItem(key, JSON.stringify(archiveBrew));
}
export function historyExists(brew){
return Object.keys(localStorage)
.some((key)=>{
return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`);
});
}
export function loadHistory(brew){
const history = {};
// Load data from local storage to History object
for (let i = 1; i <= HISTORY_SLOTS; i++){
history[i] = getVersionBySlot(brew, i);
};
return history;
}
export function updateHistory(brew) {
const history = loadHistory(brew);
// Walk each version position
for (let slot = HISTORY_SLOTS; slot > 0; slot--){
const storedVersion = history[slot];
// If slot has expired, update all lower slots and break
if(new Date() >= new Date(storedVersion.expireAt)){
for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){
// Move data from updateSlot to updateSlot + 1
!history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1);
};
// Update the most recent brew
updateStoredBrew(brew, 1);
// Break out of data checks because we found an expired value
break;
}
};
};
export function getHistoryItems(brew){
const historyArray = [];
for (let i = 1; i <= HISTORY_SLOTS; i++){
historyArray.push(getVersionBySlot(brew, i));
}
return historyArray;
};
export function versionHistoryGarbageCollection(){
Object.keys(localStorage)
.filter((key)=>{
return key.startsWith(HISTORY_PREFIX);
})
.forEach((key)=>{
const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt);
collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY);
if(new Date() > collectAt){
localStorage.removeItem(key);
}
});
};

211
package-lock.json generated
View File

@@ -23,9 +23,9 @@
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.19.2", "express": "^4.21.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.8",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
@@ -38,22 +38,22 @@
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.6.1", "mongoose": "^8.6.2",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.26.1", "react-router-dom": "6.26.2",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.1.0", "superagent": "^10.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "@stylistic/stylelint-plugin": "^3.0.1",
"eslint": "^9.9.1", "eslint": "^9.10.0",
"eslint-plugin-jest": "^28.8.3", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.35.2", "eslint-plugin-react": "^7.36.1",
"globals": "^15.9.0", "globals": "^15.9.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
@@ -2073,9 +2073,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.9.1", "version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2090,6 +2090,18 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"dev": true,
"dependencies": {
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@googleapis/drive": { "node_modules/@googleapis/drive": {
"version": "8.14.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.14.0.tgz", "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.14.0.tgz",
@@ -3013,9 +3025,9 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.19.1", "version": "1.19.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
"integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@@ -4128,10 +4140,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@@ -4141,7 +4152,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@@ -5589,10 +5600,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -5819,16 +5829,17 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.9.1", "version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0", "@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0", "@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.9.1", "@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0", "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@@ -5851,7 +5862,6 @@
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
"is-path-inside": "^3.0.3", "is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -5903,9 +5913,9 @@
} }
}, },
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.35.2", "version": "7.36.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
"integrity": "sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ==", "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"array-includes": "^3.1.8", "array-includes": "^3.1.8",
@@ -6305,37 +6315,36 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@@ -6353,12 +6362,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express-static-gzip": { "node_modules/express-static-gzip": {
"version": "2.1.7", "version": "2.1.8",
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.7.tgz", "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz",
"integrity": "sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==", "integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"serve-static": "^1.14.1" "serve-static": "^1.16.2"
} }
}, },
"node_modules/express/node_modules/cookie": { "node_modules/express/node_modules/cookie": {
@@ -6574,13 +6582,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@@ -6595,7 +6602,6 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -6603,8 +6609,7 @@
"node_modules/finalhandler/node_modules/ms": { "node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
"license": "MIT"
}, },
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
@@ -10523,10 +10528,12 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT" "funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@@ -10794,9 +10801,9 @@
} }
}, },
"node_modules/mongoose": { "node_modules/mongoose": {
"version": "8.6.1", "version": "8.6.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.1.tgz", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.2.tgz",
"integrity": "sha512-dppGcYqvsdg+VcnqXR5b467V4a+iNhmvkfYNpEPi6AjaUxnz6ioEDmrMLOi+sOWjvoHapuwPOigV4f2l7HC6ag==", "integrity": "sha512-ErbDVvuUzUfyQpXvJ6sXznmZDICD8r6wIsa0VKjJtB6/LZncqwUn5Um040G1BaNo6L3Jz+xItLSwT0wZmSmUaQ==",
"dependencies": { "dependencies": {
"bson": "^6.7.0", "bson": "^6.7.0",
"kareem": "2.6.3", "kareem": "2.6.3",
@@ -11637,10 +11644,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -12074,12 +12080,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -12204,11 +12209,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.26.1", "version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
"integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.19.1" "@remix-run/router": "1.19.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -12218,12 +12223,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.26.1", "version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
"integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.19.1", "@remix-run/router": "1.19.2",
"react-router": "6.26.1" "react-router": "6.26.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -12721,10 +12726,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -12748,7 +12752,6 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -12756,25 +12759,30 @@
"node_modules/send/node_modules/debug/node_modules/ms": { "node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
"license": "MIT" },
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
}, },
"node_modules/send/node_modules/ms": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"license": "MIT"
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -14616,21 +14624,6 @@
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/url/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/use": { "node_modules/use": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",

View File

@@ -98,9 +98,9 @@
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.19.2", "express": "^4.21.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.8",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
@@ -113,22 +113,22 @@
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.6.1", "mongoose": "^8.6.2",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.26.1", "react-router-dom": "6.26.2",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.1.0", "superagent": "^10.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "@stylistic/stylelint-plugin": "^3.0.1",
"eslint": "^9.9.1", "eslint": "^9.10.0",
"eslint-plugin-jest": "^28.8.3", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.35.2", "eslint-plugin-react": "^7.36.1",
"globals": "^15.9.0", "globals": "^15.9.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",

View File

@@ -202,6 +202,23 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
res.status(200).send(brew.text); res.status(200).send(brew.text);
}); });
//Serve brew metadata
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
const { brew } = req;
sanitizeBrew(brew, 'share');
const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
'published', 'views', 'shareId', 'createdAt', 'updatedAt',
'lastViewed', 'thumbnail', 'tags'
];
const metadata = fields.reduce((acc, field)=>{
if(brew[field] !== undefined) acc[field] = brew[field];
return acc;
}, {});
res.status(200).json(metadata);
});
//Serve brew styling //Serve brew styling
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
@@ -436,12 +453,16 @@ if(isLocalEnvironment){
//Vault Page //Vault Page
app.get('/vault', asyncHandler(async(req, res, next)=>{ app.get('/vault', asyncHandler(async(req, res, next)=>{
req.ogMeta = { ...defaultMetaTags,
title : 'The Vault',
description : 'Search for Brews'
};
return next(); return next();
})); }));
//Send rendered page //Send rendered page
app.use(asyncHandler(async (req, res, next)=>{ app.use(asyncHandler(async (req, res, next)=>{
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
const page = await renderPage(req, res); const page = await renderPage(req, res);
if(!page) return; if(!page) return;
@@ -454,7 +475,8 @@ const renderPage = async (req, res)=>{
const configuration = { const configuration = {
local : isLocalEnvironment, local : isLocalEnvironment,
publicUrl : config.get('publicUrl') ?? '', publicUrl : config.get('publicUrl') ?? '',
environment : nodeEnv environment : nodeEnv,
history : config.get('historyConfig') ?? {}
}; };
const props = { const props = {
version : require('./../package.json').version, version : require('./../package.json').version,

View File

@@ -934,7 +934,7 @@ brew`);
expect(req.brew).toEqual(testBrew); expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n'); expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200); expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith("\nI Have a style!\n"); expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
expect(res.set).toHaveBeenCalledWith({ expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache', 'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css' 'Content-Type' : 'text/css'

View File

@@ -397,6 +397,11 @@ const CodeEditor = createClass({
getCursorPosition : function(){ getCursorPosition : function(){
return this.codeMirror.getCursor(); return this.codeMirror.getCursor();
}, },
getTopVisibleLine : function(){
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
return topVisibleLine;
},
updateSize : function(){ updateSize : function(){
this.codeMirror.refresh(); this.codeMirror.refresh();
}, },

View File

@@ -105,16 +105,16 @@ renderer.link = function (href, title, text) {
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS // Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
renderer.image = function (href, title, text) { renderer.image = function (href, title, text) {
href = cleanUrl(href); href = cleanUrl(href);
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 src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
if (title) if(title)
out += ` title="${title}"`; out += ` title="${title}"`;
out += '>'; out += '>';
return out; return out;
} };
// Disable default reflink behavior, as it steps on our variables extension // Disable default reflink behavior, as it steps on our variables extension
tokenizer.def = function () { tokenizer.def = function () {
@@ -745,7 +745,7 @@ const tableTerminators = [
`:+\\n`, // hardBreak `:+\\n`, // hardBreak
` *{[^\n]+}`, // blockInjector ` *{[^\n]+}`, // blockInjector
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv ` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
] ];
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts, Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
@@ -755,12 +755,12 @@ Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
function cleanUrl(href) { function cleanUrl(href) {
try { try {
href = encodeURI(href).replace(/%25/g, '%'); href = encodeURI(href).replace(/%25/g, '%');
} catch { } catch {
return null; return null;
} }
return href; return href;
} }
const escapeTest = /[&<>"']/; const escapeTest = /[&<>"']/;

View File

@@ -42,6 +42,10 @@ const SplitPane = createClass({
}); });
} }
window.addEventListener('resize', this.handleWindowResize); window.addEventListener('resize', this.handleWindowResize);
// This lives here instead of in the initial render because you cannot touch localStorage until the componant mounts.
const loadLiveScroll = window.localStorage.getItem('liveScroll') === 'true';
this.setState({ liveScroll: loadLiveScroll });
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
@@ -89,6 +93,11 @@ const SplitPane = createClass({
userSetDividerPos : newSize userSetDividerPos : newSize
}); });
}, },
liveScrollToggle : function() {
window.localStorage.setItem('liveScroll', String(!this.state.liveScroll));
this.setState({ liveScroll: !this.state.liveScroll });
},
/* /*
unFocus : function() { unFocus : function() {
if(document.selection){ if(document.selection){
@@ -120,13 +129,18 @@ const SplitPane = createClass({
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} > onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
<i className='fas fa-arrow-right' /> <i className='fas fa-arrow-right' />
</div> </div>
<div id='scrollToggleDiv' className={this.state.liveScroll ? 'arrow lock' : 'arrow unlock'}
style={{ left: this.state.currentDividerPos-4 }}
onClick={this.liveScrollToggle} >
<i id='scrollToggle' className={this.state.liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
</div>
</>; </>;
} }
}, },
renderDivider : function(){ renderDivider : function(){
return <> return <>
{this.renderMoveArrows()} {this.props.showDividerButtons && this.renderMoveArrows()}
<div className='divider' onPointerDown={this.handleDown} > <div className='divider' onPointerDown={this.handleDown} >
<div className='dots'> <div className='dots'>
<i className='fas fa-circle' /> <i className='fas fa-circle' />
@@ -144,9 +158,10 @@ const SplitPane = createClass({
> >
{React.cloneElement(this.props.children[0], { {React.cloneElement(this.props.children[0], {
...(this.props.showDividerButtons && { ...(this.props.showDividerButtons && {
moveBrew: this.state.moveBrew, moveBrew : this.state.moveBrew,
moveSource: this.state.moveSource, moveSource : this.state.moveSource,
setMoveArrows: this.setMoveArrows, liveScroll : this.state.liveScroll,
setMoveArrows : this.setMoveArrows,
}), }),
})} })}
</Pane> </Pane>

View File

@@ -53,6 +53,15 @@
.tooltipRight('Jump to location in Preview'); .tooltipRight('Jump to location in Preview');
top : 60px; top : 60px;
} }
&.lock{
.tooltipRight('De-sync Editor and Preview locations.');
top : 90px;
background: #666;
}
&.unlock{
.tooltipRight('Sync Editor and Preview locations');
top : 90px;
}
&:hover{ &:hover{
background-color: #666; background-color: #666;
} }

View File

@@ -27,35 +27,154 @@ module.exports = [
experimental : true, experimental : true,
subsnippets : [ subsnippets : [
{ {
name : 'Table of Contents', name : 'Generate Table of Contents',
icon : 'fas fa-book', icon : 'fas fa-book',
gen : TableOfContentsGen, gen : TableOfContentsGen,
experimental : true experimental : true
}, },
{ {
name : 'Include in ToC up to H3', name : 'Table of Contents Individual Inclusion',
icon : 'fas fa-dice-three', icon : 'fas fa-book',
gen : dedent `\n{{tocInclude# CHANGE # to your header level
}}\n`,
subsnippets : [
{
name : 'Individual Inclusion H1',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH1 \n
}}\n`,
},
{
name : 'Individual Inclusion H2',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH2 \n
}}\n`,
},
{
name : 'Individual Inclusion H3',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH3 \n
}}\n`,
},
{
name : 'Individual Inclusion H4',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH4 \n
}}\n`,
},
{
name : 'Individual Inclusion H5',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH5 \n
}}\n`,
},
{
name : 'Individual Inclusion H6',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH6 \n
}}\n`,
}
]
},
{
name : 'Table of Contents Range Inclusion',
icon : 'fas fa-book',
gen : dedent `\n{{tocDepthH3 gen : dedent `\n{{tocDepthH3
}}\n`, }}\n`,
subsnippets : [
{
name : 'Include in ToC up to H3',
icon : 'fas fa-dice-three',
gen : dedent `\n{{tocDepthH3
}}\n`,
},
{
name : 'Include in ToC up to H4',
icon : 'fas fa-dice-four',
gen : dedent `\n{{tocDepthH4
}}\n`,
},
{
name : 'Include in ToC up to H5',
icon : 'fas fa-dice-five',
gen : dedent `\n{{tocDepthH5
}}\n`,
},
{
name : 'Include in ToC up to H6',
icon : 'fas fa-dice-six',
gen : dedent `\n{{tocDepthH6
}}\n`,
},
]
}, },
{ {
name : 'Include in ToC up to H4', name : 'Table of Contents Individual Exclusion',
icon : 'fas fa-dice-four', icon : 'fas fa-book',
gen : dedent `\n{{tocDepthH4 gen : dedent `\n{{tocExcludeH1 \n
}}\n`, }}\n`,
subsnippets : [
{
name : 'Individual Exclusion H1',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH1 \n
}}\n`,
},
{
name : 'Individual Exclusion H2',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH2 \n
}}\n`,
},
{
name : 'Individual Exclusion H3',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH3 \n
}}\n`,
},
{
name : 'Individual Exclusion H4',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH4 \n
}}\n`,
},
{
name : 'Individual Exclusion H5',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH5 \n
}}\n`,
},
{
name : 'Individual Exclusion H6',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH6 \n
}}\n`,
},
]
}, },
{ {
name : 'Include in ToC up to H5', name : 'Table of Contents Toggles',
icon : 'fas fa-dice-five', icon : 'fas fa-book',
gen : dedent `\n{{tocDepthH5 gen : `{{tocGlobalH4}}\n\n`,
}}\n`, subsnippets : [
}, {
{ name : 'Enable H1-H4 all pages',
name : 'Include in ToC up to H6', icon : 'fas fa-dice-four',
icon : 'fas fa-dice-six', gen : `{{tocGlobalH4}}\n\n`,
gen : dedent `\n{{tocDepthH6 },
}}\n`, {
name : 'Enable H1-H5 all pages',
icon : 'fas fa-dice-five',
gen : `{{tocGlobalH5}}\n\n`,
},
{
name : 'Enable H1-H6 all pages',
icon : 'fas fa-dice-six',
gen : `{{tocGlobalH6}}\n\n`,
},
]
} }
] ]
}, },
@@ -94,7 +213,7 @@ module.exports = [
background-image: linear-gradient(-45deg, #322814, #998250, #322814); background-image: linear-gradient(-45deg, #322814, #998250, #322814);
line-height: 1em; line-height: 1em;
}\n\n` }\n\n`
} },
] ]
}, },

View File

@@ -1,77 +1,78 @@
const _ = require('lodash');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const getTOC = (pages)=>{ // Map each actual page to its footer label, accounting for skips or numbering resets
const mapPages = (pages)=>{
let actualPage = 0;
let mappedPage = 0; // Number displayed in footer
const pageMap = [];
const recursiveAdd = (title, page, targetDepth, child, curDepth=0)=>{ pages.forEach((page)=>{
if(curDepth > 5) return; // Something went wrong. actualPage++;
if(curDepth == targetDepth) { const doSkip = page.querySelector('.skipCounting');
child.push({ const doReset = page.querySelector('.resetCounting');
title : title,
page : page, if(doReset)
children : [] mappedPage = 1;
}); if(!doSkip && !doReset)
} else { mappedPage++;
if(child.length == 0) {
child.push({ pageMap[actualPage] = {
title : null, mappedPage : mappedPage,
page : page, showPage : !doSkip
children : [] };
}); });
return pageMap;
};
const getMarkdown = (headings, pageMap)=>{
const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -'];
const allMarkdown = [];
const depthChain = [0];
headings.forEach((heading)=>{
const page = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
const mappedPage = pageMap[page].mappedPage;
const showPage = pageMap[page].showPage;
const title = heading.textContent.trim();
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
const depth = parseInt(heading.tagName.substring(1));
if(!title || !showPage || ToCExclude == 'exclude')
return;
//If different header depth than last, remove indents until nearest higher-level header, then indent once
if(depth !== depthChain[depthChain.length -1]) {
while (depth <= depthChain[depthChain.length - 1]) {
depthChain.pop();
} }
recursiveAdd(title, page, targetDepth, _.last(child).children, curDepth+1,); depthChain.push(depth);
} }
};
const res = []; const markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`;
allMarkdown.push(markdown);
});
return allMarkdown.join('\n');
};
const getTOC = ()=>{
const iframe = document.getElementById('BrewRenderer'); const iframe = document.getElementById('BrewRenderer');
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document; const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6'); const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6');
const headerDepth = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']; const pages = iframeDocument.querySelectorAll('.page');
_.each(headings, (heading)=>{ const pageMap = mapPages(pages);
const onPage = parseInt(heading.closest('.page').id?.replace(/^p/, '')); return getMarkdown(headings, pageMap);
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
if(ToCExclude != 'exclude') {
recursiveAdd(heading.textContent.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
}
});
return res;
};
const ToCIterate = (entries, curDepth=0)=>{
const levelPad = ['- ###', ' - ####', ' - ', ' - ', ' - ', ' - '];
const toc = [];
if(entries.title !== null){
toc.push(`${levelPad[curDepth]} [{{ ${entries.title}}}{{ ${entries.page}}}](#p${entries.page})`);
}
if(entries.children.length) {
_.each(entries.children, (entry, idx)=>{
const children = ToCIterate(entry, entry.title == null ? curDepth : curDepth+1);
if(children.length) {
toc.push(...children);
}
});
}
return toc;
}; };
module.exports = function(props){ module.exports = function(props){
const pages = props.brew.text.split('\\page'); const TOC = getTOC();
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(ToCIterate(g1).join('\n'));
return r;
}, []).join('\n');
return dedent` return dedent`
{{toc,wide {{toc,wide
# Contents # Contents
${markdown} ${TOC}
}} }}
\n`; \n`;
}; };

View File

@@ -11,6 +11,7 @@
--HB_Color_CaptionText : #766649; // Brown --HB_Color_CaptionText : #766649; // Brown
--HB_Color_WatercolorStain : #BBAD82; // Light brown --HB_Color_WatercolorStain : #BBAD82; // Light brown
--HB_Color_Footnotes : #C9AD6A; // Gold --HB_Color_Footnotes : #C9AD6A; // Gold
--TOC : 'include';
} }
.useSansSerif() { .useSansSerif() {
@@ -797,7 +798,7 @@
// *****************************/ // *****************************/
// Default Exclusions // Default Exclusions
// Anything not exlcuded is included, default Headers are H1, H2, and H3. // Anything not excluded is included, default Headers are H1, H2, and H3.
h4, h4,
h5, h5,
h6, h6,
@@ -808,12 +809,23 @@ h6,
.noToC, .noToC,
.toc { --TOC: exclude; } .toc { --TOC: exclude; }
.tocDepthH2 :is(h1, h2) {--TOC: include; }
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
// Brew level default inclusion changes.
// These add Headers 'back' to inclusion.
.pages:has(.tocGlobalH4) {
h4 {--TOC: include; }
}
.pages:has(.tocGlobalH5) {
h4, h5 {--TOC: include; }
}
.pages:has(.tocGlobalH6) {
h4, h5, h6 {--TOC: include; }
}
// Block level inclusion changes
// These include either a single (include) or a range (depth)
.tocIncludeH1 h1 {--TOC: include; } .tocIncludeH1 h1 {--TOC: include; }
.tocIncludeH2 h2 {--TOC: include; } .tocIncludeH2 h2 {--TOC: include; }
.tocIncludeH3 h3 {--TOC: include; } .tocIncludeH3 h3 {--TOC: include; }
@@ -821,6 +833,21 @@ h6,
.tocIncludeH5 h5 {--TOC: include; } .tocIncludeH5 h5 {--TOC: include; }
.tocIncludeH6 h6 {--TOC: include; } .tocIncludeH6 h6 {--TOC: include; }
.tocDepthH2 :is(h1, h2) {--TOC: include; }
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
// Block level exclusion changes
// These exclude a single block level
.tocExcludeH1 h1 {--TOC: exclude; }
.tocExcludeH2 h2 {--TOC: exclude; }
.tocExcludeH3 h3 {--TOC: exclude; }
.tocExcludeH4 h4 {--TOC: exclude; }
.tocExcludeH5 h5 {--TOC: exclude; }
.tocExcludeH6 h6 {--TOC: exclude; }
.page:has(.partCover) { .page:has(.partCover) {
--TOC: exclude; --TOC: exclude;
& h1 { & h1 {

View File

@@ -23,14 +23,30 @@ module.exports = [
gen : '\n\\page\n' gen : '\n\\page\n'
}, },
{ {
name : 'Page Number', name : 'Page Numbering',
icon : 'fas fa-bookmark', icon : 'fas fa-bookmark',
gen : '{{pageNumber 1}}\n' subsnippets : [
}, {
{ name : 'Page Number',
name : 'Auto-incrementing Page Number', icon : 'fas fa-bookmark',
icon : 'fas fa-sort-numeric-down', gen : '{{pageNumber 1}}\n'
gen : '{{pageNumber,auto}}\n' },
{
name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down',
gen : '{{pageNumber,auto}}\n'
},
{
name : 'Skip Page Number Increment this Page',
icon : 'fas fa-xmark',
gen : '{{skipCounting}}\n'
},
{
name : 'Restart Numbering',
icon : 'fas fa-arrow-rotate-left',
gen : '{{resetCounting}}\n'
},
]
}, },
{ {
name : 'Footer', name : 'Footer',
@@ -352,6 +368,11 @@ module.exports = [
icon : 'font MrEavesRemake', icon : 'font MrEavesRemake',
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}` gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
}, },
{
name : 'Pagella',
icon : 'font Pagella',
gen : dedent`{{font-family:Pagella Dummy Text}}`
},
{ {
name : 'Solbera Imitation', name : 'Solbera Imitation',
icon : 'font SolberaImitationRemake', icon : 'font SolberaImitationRemake',
@@ -410,22 +431,40 @@ module.exports = [
] ]
}, },
/**************** PAGE *************/ /**************** LAYOUT *************/
{ {
groupName : 'Print', groupName : 'Print',
icon : 'fas fa-print', icon : 'fas fa-print',
view : 'style', view : 'style',
snippets : [ snippets : [
{
name : 'A3 Page Size',
icon : 'far fa-file',
gen : dedent`/* A3 Page Size */
.page {
width : 297mm;
height : 420mm;
}\n\n`,
},
{ {
name : 'A4 Page Size', name : 'A4 Page Size',
icon : 'far fa-file', icon : 'far fa-file',
gen : dedent`/* A4 Page Size */ gen : dedent`/* A4 Page Size */
.page{ .page {
width : 210mm; width : 210mm;
height : 296.8mm; height : 296.8mm;
}\n\n` }\n\n`
}, },
{
name : 'A5 Page Size',
icon : 'far fa-file',
gen : dedent`/* A5 Page Size */
.page {
width : 148mm;
height : 210mm;
}\n\n`,
},
{ {
name : 'Square Page Size', name : 'Square Page Size',
icon : 'far fa-file', icon : 'far fa-file',
@@ -437,6 +476,17 @@ module.exports = [
columns : unset; columns : unset;
}\n\n` }\n\n`
}, },
{
name : 'Card Page Size',
icon : 'far fa-file',
gen : dedent`/* Card Size */
.page {
width : 63.5mm;
height : 88.9mm;
padding : 5mm;
columns : unset;
}\n\n`
},
{ {
name : 'Ink Friendly', name : 'Ink Friendly',
icon : 'fas fa-tint', icon : 'fas fa-tint',
@@ -452,5 +502,5 @@ module.exports = [
}\n\n` }\n\n`
}, },
] ]
} },
]; ];

View File

@@ -1,3 +1,4 @@
@import (less) './themes/fonts/Blank/fonts.less';
@import (less) './themes/fonts/5e/fonts.less'; @import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less'; @import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/iconFonts/elderberryInn.less'; @import (less) './themes/fonts/iconFonts/elderberryInn.less';
@@ -12,7 +13,7 @@
} }
@page { margin : 0; } @page { margin : 0; }
body { counter-reset : page-numbers; } body { counter-reset : page-numbers 0; }
* { -webkit-print-color-adjust : exact; } * { -webkit-print-color-adjust : exact; }
//***************************** //*****************************
@@ -51,7 +52,6 @@ body { counter-reset : page-numbers; }
height : 279.4mm; height : 279.4mm;
padding : 1.4cm 1.9cm 1.7cm; padding : 1.4cm 1.9cm 1.7cm;
overflow : hidden; overflow : hidden;
counter-increment : page-numbers;
background-color : var(--HB_Color_Background); background-color : var(--HB_Color_Background);
text-rendering : optimizeLegibility; text-rendering : optimizeLegibility;
contain : size; contain : size;
@@ -494,4 +494,13 @@ body { counter-reset : page-numbers; }
&:nth-child(even) { &:nth-child(even) {
.pageNumber { left : 30px; } .pageNumber { left : 30px; }
} }
.resetCounting {
counter-set : page-numbers 1;
}
&:not(:has(.skipCounting)) {
counter-increment : page-numbers;
}
} }

View File

@@ -0,0 +1,46 @@
/*
TeX Gyre Pagella
License:
% Copyright 2007--2018 for TeX Gyre extensions by B. Jackowski,
% J.M. Nowacki et al. (on behalf of TeX Users Groups). Vietnamese
% characters were added by Han The Thanh.
%
% This work can be freely used and distributed under
% the GUST Font License (GFL -- see GUST-FONT-LICENSE.txt)
% which is actually an instance of the LaTeX Project Public License
% (LPPL -- see http://www.latex-project.org/lppl.txt ).
%
% This work has the maintenance status "maintained". The Current Maintainer
% of this work is Bogus\l{}aw Jackowski and Janusz M. Nowacki.
%
% This work consists of the files listed
% in the MANIFEST-TeX-Gyre-Pagella.txt file.
*/
@font-face {
font-family: Pagella;
src: url('../../../fonts/Blank/texgyrepagella-regular.woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: Pagella;
src: url('../../../fonts/Blank/texgyrepagella-bold.woff2');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: Pagella;
src: url('../../../fonts/Blank/texgyrepagella-italic.woff2');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: Pagella;
src: url('../../../fonts/Blank/texgyrepagella-bolditalic.woff2');
font-weight: bold;
font-style: italic;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.