0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-07 01:12:44 +00:00

Merge branch 'master' into GlobalToCToggles

This commit is contained in:
Trevor Buckner
2024-09-16 09:45:35 -04:00
committed by GitHub
19 changed files with 205 additions and 154 deletions

View File

@@ -92,7 +92,7 @@ const BrewRenderer = (props)=>{
const updateCurrentPage = useCallback(_.throttle((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 + 1) / totalScrollableHeight) * rawPages.length); const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
props.onPageChange(currentPageNumber); props.onPageChange(currentPageNumber);
}, 200), []); }, 200), []);

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

@@ -56,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,9 +67,9 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
return deltaZoom; return deltaZoom;
}; };
return ( return (
<div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}> <div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}>
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible)}}><i className='fas fa-glasses' /></button> <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

@@ -3,7 +3,6 @@ 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',
@@ -36,16 +36,16 @@ const Editor = createClass({
onStyleChange : ()=>{}, onStyleChange : ()=>{},
onMetaChange : ()=>{}, onMetaChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
onViewPageChange : ()=>{}, onViewPageChange : ()=>{},
editorTheme : 'default', editorTheme : 'default',
renderer : 'legacy', renderer : 'legacy',
currentEditorCursorPageNum : 0, currentEditorCursorPageNum : 1,
currentEditorViewPageNum : 0, currentEditorViewPageNum : 1,
currentBrewRendererPageNum : 0, currentBrewRendererPageNum : 1,
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -63,14 +63,15 @@ 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('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine())}, 200)); 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) {
@@ -85,22 +86,32 @@ 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();
} }
@@ -117,14 +128,14 @@ const Editor = createClass({
updateCurrentCursorPage : function(cursor) { updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1); const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/; const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
const currentPage = lines.reduce((count, line) => count + (pageRegex.test(line) ? 1 : 0), 1); const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onCursorPageChange(currentPage); this.props.onCursorPageChange(currentPage);
}, },
updateCurrentViewPage : function(topScrollLine) { updateCurrentViewPage : function(topScrollLine) {
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1); const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/; const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
const currentPage = lines.reduce((count, line) => count + (pageRegex.test(line) ? 1 : 0), 1); const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onViewPageChange(currentPage); this.props.onViewPageChange(currentPage);
}, },
@@ -156,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
@@ -174,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
@@ -200,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){
@@ -208,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' });
} }
} }
} }
@@ -221,12 +232,12 @@ const Editor = createClass({
let startIndex = line.indexOf('^'); let startIndex = line.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy; const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy; const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
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' });
} }
@@ -276,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();
}); });
@@ -302,64 +313,93 @@ const Editor = createClass({
} }
}, },
brewJump : function(targetPage=this.props.currentEditorCursorPageNum){ brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window) return; if(!window || isJumping)
return;
// Get current brewRenderer scroll position and calculate target position // 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 textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/; const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
const textString = this.props.brew.text.split(textSplit).slice(0, this.props.currentBrewRendererPageNum-1).join(textSplit); const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
const textPosition = textString.length; const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
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);
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; const checkIfScrollComplete = ()=>{
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); 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
};
//Scroll 1/10 of the way every 10ms until 1px off. isJumping = true;
const incrementalScroll = setInterval(()=>{ checkIfScrollComplete();
currentY += (targetY - currentY) / 10; this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window if(smooth) {
if(Math.abs(targetY - currentY > 100)) //Scroll 1/10 of the way every 10ms until 1px off.
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// End when close enough // Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY) < 1) { if(Math.abs(targetY - currentY > 100))
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); // End when close enough
clearInterval(incrementalScroll); if(Math.abs(targetY - currentY) < 1) {
} this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
}, 10); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
} this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
}
}, 10);
} else {
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');
} }
}, },
@@ -389,8 +429,6 @@ const Editor = createClass({
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onTextChange} onChange={this.props.onTextChange}
onCursorActivity={this.props.onCursorActivity}
onScroll={this.props.onPageChange}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} /> rerenderParent={this.rerenderParent} />
</>; </>;

View File

@@ -70,7 +70,7 @@ const Snippetbar = createClass({
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);
} }
}, },

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

@@ -55,9 +55,9 @@ const EditPage = createClass({
autoSave : true, autoSave : true,
autoSaveWarning : false, autoSaveWarning : false,
unsavedTime : new Date(), unsavedTime : new Date(),
currentEditorViewPageNum : 0, currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 0, currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 0, currentBrewRendererPageNum : 1,
displayLockMessage : this.props.brew.lock || false, displayLockMessage : this.props.brew.lock || false,
themeBundle : {} themeBundle : {}
}; };
@@ -117,23 +117,19 @@ const EditPage = createClass({
}, },
handleEditorViewPageChange : function(pageNumber){ handleEditorViewPageChange : function(pageNumber){
console.log(`editor view : ${pageNumber}`);
this.setState({ currentEditorViewPageNum: pageNumber }); this.setState({ currentEditorViewPageNum: pageNumber });
}, },
handleEditorCursorPageChange : function(pageNumber){ handleEditorCursorPageChange : function(pageNumber){
console.log(`editor cursor : ${pageNumber}`);
this.setState({ currentEditorCursorPageNum: pageNumber }); this.setState({ currentEditorCursorPageNum: pageNumber });
}, },
handleBrewRendererPageChange : function(pageNumber){ handleBrewRendererPageChange : function(pageNumber){
console.log(`brewRenderer view : ${pageNumber}`);
this.setState({ currentBrewRendererPageNum: 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
console.log('text change');
let htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);

View File

@@ -34,9 +34,9 @@ const HomePage = createClass({
brew : this.props.brew, brew : this.props.brew,
welcomeText : this.props.brew.text, welcomeText : this.props.brew.text,
error : undefined, error : undefined,
currentEditorViewPageNum : 0, currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 0, currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 0, currentBrewRendererPageNum : 1,
themeBundle : {} themeBundle : {}
}; };
}, },
@@ -64,17 +64,14 @@ const HomePage = createClass({
}, },
handleEditorViewPageChange : function(pageNumber){ handleEditorViewPageChange : function(pageNumber){
console.log(`editor view : ${pageNumber}`);
this.setState({ currentEditorViewPageNum: pageNumber }); this.setState({ currentEditorViewPageNum: pageNumber });
}, },
handleEditorCursorPageChange : function(pageNumber){ handleEditorCursorPageChange : function(pageNumber){
console.log(`editor cursor : ${pageNumber}`);
this.setState({ currentEditorCursorPageNum: pageNumber }); this.setState({ currentEditorCursorPageNum: pageNumber });
}, },
handleBrewRendererPageChange : function(pageNumber){ handleBrewRendererPageChange : function(pageNumber){
console.log(`brewRenderer view : ${pageNumber}`);
this.setState({ currentBrewRendererPageNum: pageNumber }); this.setState({ currentBrewRendererPageNum: pageNumber });
}, },

View File

@@ -44,9 +44,9 @@ const NewPage = createClass({
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),
currentEditorViewPageNum : 0, currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 0, currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 0, currentBrewRendererPageNum : 1,
themeBundle : {} themeBundle : {}
}; };
}, },
@@ -111,17 +111,14 @@ const NewPage = createClass({
}, },
handleEditorViewPageChange : function(pageNumber){ handleEditorViewPageChange : function(pageNumber){
console.log(`editor view : ${pageNumber}`);
this.setState({ currentEditorViewPageNum: pageNumber }); this.setState({ currentEditorViewPageNum: pageNumber });
}, },
handleEditorCursorPageChange : function(pageNumber){ handleEditorCursorPageChange : function(pageNumber){
console.log(`editor cursor : ${pageNumber}`);
this.setState({ currentEditorCursorPageNum: pageNumber }); this.setState({ currentEditorCursorPageNum: pageNumber });
}, },
handleBrewRendererPageChange : function(pageNumber){ handleBrewRendererPageChange : function(pageNumber){
console.log(`brewRenderer view : ${pageNumber}`);
this.setState({ currentBrewRendererPageNum: pageNumber }); this.setState({ currentBrewRendererPageNum: pageNumber });
}, },

View File

@@ -330,7 +330,7 @@ const VaultPage = (props)=>{
if(error) { if(error) {
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || ''; const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
return ( return (
<div className='foundBrews noBrews'> <div className='foundBrews noBrews'>
<h3>Error: {errorText}</h3> <h3>Error: {errorText}</h3>

11
package-lock.json generated
View File

@@ -25,7 +25,7 @@
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.0", "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",
@@ -6362,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": {

View File

@@ -100,7 +100,7 @@
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.0", "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",

View File

@@ -203,22 +203,22 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
}); });
//Serve brew metadata //Serve brew metadata
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res) => { app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
const { brew } = req; const { brew } = req;
sanitizeBrew(brew, 'share'); sanitizeBrew(brew, 'share');
const fields = [ 'title', 'pageCount', 'description', 'authors', 'lang', const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
'published', 'views', 'shareId', 'createdAt', 'updatedAt', 'published', 'views', 'shareId', 'createdAt', 'updatedAt',
'lastViewed', 'thumbnail', 'tags' 'lastViewed', 'thumbnail', 'tags'
]; ];
const metadata = fields.reduce((acc, field) => { const metadata = fields.reduce((acc, field)=>{
if (brew[field] !== undefined) acc[field] = brew[field]; if(brew[field] !== undefined) acc[field] = brew[field];
return acc; return acc;
}, {}); }, {});
res.status(200).json(metadata); 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);});
@@ -378,7 +378,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
app.get('/account', asyncHandler(async (req, res, next)=>{ app.get('/account', asyncHandler(async (req, res, next)=>{
const data = {}; const data = {};
data.title = 'Account Information Page'; data.title = 'Account Information Page';
if(!req.account) { if(!req.account) {
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"'); res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
const error = new Error('No valid account'); const error = new Error('No valid account');
@@ -462,8 +462,8 @@ app.get('/vault', asyncHandler(async(req, res, 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;
res.send(page); res.send(page);

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

@@ -49,12 +49,12 @@ const CodeEditor = createClass({
displayName : 'CodeEditor', displayName : 'CodeEditor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
language : '', language : '',
value : '', value : '',
wrap : true, wrap : true,
onChange : ()=>{}, onChange : ()=>{},
enableFolding : true, enableFolding : true,
editorTheme : 'default' editorTheme : 'default'
}; };
}, },
@@ -189,7 +189,7 @@ const CodeEditor = createClass({
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror); autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works. // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue())}); this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
this.updateSize(); this.updateSize();
}, },
@@ -399,7 +399,7 @@ const CodeEditor = createClass({
}, },
getTopVisibleLine : function(){ getTopVisibleLine : function(){
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect(); const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, "window"); const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
return topVisibleLine; return topVisibleLine;
}, },
updateSize : function(){ updateSize : function(){

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,6 +129,11 @@ 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>
</>; </>;
} }
}, },
@@ -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

@@ -4,9 +4,9 @@ const dedent = require('dedent-tabs').default;
const mapPages = (pages)=>{ const mapPages = (pages)=>{
let actualPage = 0; let actualPage = 0;
let mappedPage = 0; // Number displayed in footer let mappedPage = 0; // Number displayed in footer
let pageMap = []; const pageMap = [];
pages.forEach(page => { pages.forEach((page)=>{
actualPage++; actualPage++;
const doSkip = page.querySelector('.skipCounting'); const doSkip = page.querySelector('.skipCounting');
const doReset = page.querySelector('.resetCounting'); const doReset = page.querySelector('.resetCounting');
@@ -24,13 +24,13 @@ const mapPages = (pages)=>{
return pageMap; return pageMap;
}; };
const getMarkdown = (headings, pageMap) => { const getMarkdown = (headings, pageMap)=>{
const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -']; const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -'];
let allMarkdown = [];
let depthChain = [0];
headings.forEach(heading => { const allMarkdown = [];
const depthChain = [0];
headings.forEach((heading)=>{
const page = parseInt(heading.closest('.page').id?.replace(/^p/, '')); const page = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
const mappedPage = pageMap[page].mappedPage; const mappedPage = pageMap[page].mappedPage;
const showPage = pageMap[page].showPage; const showPage = pageMap[page].showPage;
@@ -42,14 +42,14 @@ const getMarkdown = (headings, pageMap) => {
return; return;
//If different header depth than last, remove indents until nearest higher-level header, then indent once //If different header depth than last, remove indents until nearest higher-level header, then indent once
if (depth !== depthChain[depthChain.length -1]) { if(depth !== depthChain[depthChain.length -1]) {
while (depth <= depthChain[depthChain.length - 1]) { while (depth <= depthChain[depthChain.length - 1]) {
depthChain.pop(); depthChain.pop();
} }
depthChain.push(depth); depthChain.push(depth);
} }
let markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`; const markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`;
allMarkdown.push(markdown); allMarkdown.push(markdown);
}); });
return allMarkdown.join('\n'); return allMarkdown.join('\n');