0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-07 16:22:42 +00:00

Merge branch 'Fill-Pane-Buttons' into View-Modes

This commit is contained in:
Gazook89
2024-08-23 23:26:24 -05:00
17 changed files with 15696 additions and 15737 deletions

View File

@@ -67,6 +67,9 @@ jobs:
- run: - run:
name: Test - Definition Lists name: Test - Definition Lists
command: npm run test:definition-lists command: npm run test:definition-lists
- run:
name: Test - Hard Breaks
command: npm run test:hard-breaks
- run: - run:
name: Test - Variables name: Test - Variables
command: npm run test:variables command: npm run test:variables

View File

@@ -1,79 +0,0 @@
module.exports = {
root : true,
parserOptions : {
ecmaVersion : 2021,
sourceType : 'module',
ecmaFeatures : {
jsx : true
}
},
env : {
browser : true,
node : true
},
plugins : ['react', 'jest'],
rules : {
/** Errors **/
'camelcase' : ['error', { properties: 'never' }],
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error',
'no-iterator' : 'error',
'no-nested-ternary' : 'error',
'no-new-object' : 'error',
'no-proto' : 'error',
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
'react/jsx-uses-react' : 'error',
'react/prefer-es6-class' : ['error', 'never'],
'jest/valid-expect' : ['error', { maxArgs: 3 }],
/** Warnings **/
'max-lines' : ['warn', {
max : 200,
skipComments : true,
skipBlankLines : true,
}],
'max-depth' : ['warn', { max: 4 }],
'max-params' : ['warn', { max: 5 }],
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
'no-unused-vars' : ['warn', {
vars : 'all',
args : 'none',
varsIgnorePattern : 'config|_|cx|createClass'
}],
'react/jsx-uses-vars' : 'warn',
/** Fixable **/
'arrow-parens' : ['warn', 'always'],
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
'jsx-quotes' : ['warn', 'prefer-single'],
'no-var' : 'warn',
'prefer-const' : 'warn',
'prefer-template' : 'warn',
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
'semi' : ['warn', 'always'],
/** Whitespace **/
'array-bracket-spacing' : ['warn', 'never'],
'arrow-spacing' : ['warn', { before: false, after: false }],
'comma-spacing' : ['warn', { before: false, after: true }],
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
'keyword-spacing' : ['warn', {
before : true,
after : true,
overrides : {
if : { 'before': false, 'after': false }
}
}],
'key-spacing' : ['warn', {
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
singleLine : { beforeColon: false, afterColon: true }
}],
'linebreak-style' : 'off',
'no-trailing-spaces' : 'warn',
'no-whitespace-before-property' : 'warn',
'object-curly-spacing' : ['warn', 'always'],
'react/jsx-indent-props' : ['warn', 'tab'],
'space-in-parens' : ['warn', 'never'],
'template-curly-spacing' : ['warn', 'never'],
}
};

View File

@@ -61,12 +61,11 @@ const BrewRenderer = (props)=>{
}; };
const [state, setState] = useState({ const [state, setState] = useState({
viewablePageNumber : 0, height : PAGE_HEIGHT,
height : PAGE_HEIGHT, isMounted : false,
isMounted : false, visibility : 'hidden',
visibility : 'hidden', zoom : 100,
zoom : 100, currentPageNumber : 1,
currentPageNumber : 1,
}); });
const mainRef = useRef(null); const mainRef = useRef(null);
@@ -90,16 +89,6 @@ const BrewRenderer = (props)=>{
})); }));
}; };
const handleScroll = (e)=>{
const target = e.target;
setState((prevState)=>({
...prevState,
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
}));
getCurrentPage(e);
};
const getCurrentPage = (e) => { const getCurrentPage = (e) => {
const target = e.target; const target = e.target;
const { scrollTop, clientHeight, scrollHeight } = target; const { scrollTop, clientHeight, scrollHeight } = target;
@@ -109,7 +98,7 @@ const BrewRenderer = (props)=>{
setState((prevState) => ({ setState((prevState) => ({
...prevState, ...prevState,
currentPageNumber: currentPageNumber || 1 currentPageNumber : currentPageNumber || 1
})); }));
}; };
@@ -120,23 +109,12 @@ const BrewRenderer = (props)=>{
if(index == props.currentEditorPage) //Already rendered before this step if(index == props.currentEditorPage) //Already rendered before this step
return false; return false;
if(Math.abs(index - state.viewablePageNumber) <= 3) if(Math.abs(index - state.currentPageNumber) <= 3)
return true; return true;
return false; return false;
}; };
const renderPageInfo = ()=>{
return <div className='pageInfo' ref={mainRef}>
<div>
{props.renderer}
</div>
<div>
{state.viewablePageNumber + 1} / {rawPages.length}
</div>
</div>;
};
const renderDummyPage = (index)=>{ const renderDummyPage = (index)=>{
return <div className='phb page' id={`p${index + 1}`} key={index}> return <div className='phb page' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' /> <i className='fas fa-spinner fa-spin' />
@@ -219,7 +197,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={handleScroll}> ? <div className='brewRenderer' onScroll={getCurrentPage}>
<div className='pages'> <div className='pages'>
{renderDummyPage(1)} {renderDummyPage(1)}
</div> </div>
@@ -227,7 +205,7 @@ const BrewRenderer = (props)=>{
: null} : null}
<ErrorBar errors={props.errors} /> <ErrorBar errors={props.errors} />
<div className='popups'> <div className='popups' ref={mainRef}>
<RenderWarnings /> <RenderWarnings />
<NotificationPopup /> <NotificationPopup />
</div> </div>
@@ -242,17 +220,11 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={handleScroll} onScroll={getCurrentPage}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={{ height: state.height }}> style={{ height: state.height }}>
<ErrorBar errors={props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
{/* Apply CSS from Style tab and render pages from Markdown tab */} {/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted {state.isMounted
&& &&
@@ -265,7 +237,6 @@ const BrewRenderer = (props)=>{
} }
</div> </div>
</Frame> </Frame>
{renderPageInfo()}
</> </>
); );
}; };

View File

@@ -1,9 +1,9 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less'; @import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer { .brewRenderer {
will-change : transform; padding-top : 30px;
overflow-y : scroll; overflow-y : scroll;
padding-block : 30px; will-change : transform;
:where(.pages) { :where(.pages) {
margin : 30px 30px; margin : 30px 30px;
@@ -44,68 +44,33 @@
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 20px; width : 20px;
&:horizontal{ &:horizontal {
height: 20px; width : auto;
width:auto; height : 20px;
} }
&-thumb { &-thumb {
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px); background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
&:horizontal{ &:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
} }
&-corner { &-corner { visibility : hidden; }
visibility: hidden;
}
} }
} }
.pane { position : relative; } .pane { position : relative; }
.pageInfo {
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
div {
display : inline-block;
padding : 8px 10px;
&:not(:last-child) { border-right : 1px solid #666666; }
}
}
.ppr_msg {
position : absolute;
bottom : 0;
left : 0px;
z-index : 1000;
padding : 8px 10px;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
}
@media print { @media print {
.toolBar { .toolBar { display : none; }
display: none;
}
.brewRenderer { .brewRenderer {
height: 100%; height : 100%;
overflow-y: unset; padding-top : unset;
overflow-y : unset;
.pages { .pages {
margin: 0px; margin : 0px;
&>.page { & > .page { box-shadow : unset; }
box-shadow: unset;
}
} }
} }
} }

View File

@@ -1,9 +1,10 @@
.popups { .popups {
position : fixed; position : fixed;
top : @navbarHeight; top : calc(@navbarHeight + @viewerToolsHeight);
right : 24px; right : 24px;
z-index : 10001; z-index : 10001;
width : 450px; width : 450px;
padding-top : 5px;
} }
.notificationPopup { .notificationPopup {

View File

@@ -1,7 +1,7 @@
require('./toolBar.less'); require('./toolBar.less');
const React = require('react'); const React = require('react');
const { useState, useEffect } = React; const { useState, useEffect } = React;
const _ = require('lodash') const _ = require('lodash');
import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx'; import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
@@ -11,7 +11,7 @@ const MIN_ZOOM = 10;
const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100); const [zoomLevel, setZoomLevel] = useState(100);
const [pageInput, setPageInput] = useState(currentPage); const [pageNum, setPageNum] = useState(currentPage);
const [arrangement, setArrangement] = useState('single'); const [arrangement, setArrangement] = useState('single');
const modes = ['single', 'facing', 'flow']; const modes = ['single', 'facing', 'flow'];
@@ -20,8 +20,8 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
}, [zoomLevel]); }, [zoomLevel]);
useEffect(()=>{ useEffect(()=>{
setPageInput(currentPage); setPageNum(currentPage);
}, [currentPage]); }, [currentPage]);;
// update display arrangement when arrangement state is changed. // update display arrangement when arrangement state is changed.
useEffect(()=>{ useEffect(()=>{
@@ -35,17 +35,12 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
}, [arrangement]); }, [arrangement]);
const handleZoomChange = (delta)=>{ const handleZoomButton = (delta)=>{
const zoomChange = _.clamp(zoomLevel + delta, MIN_ZOOM, MAX_ZOOM); const newZoomLevel = _.round(_.clamp(zoomLevel + delta, MIN_ZOOM, MAX_ZOOM));
setZoomLevel(newZoomLevel);
setZoomLevel(zoomChange);
}; };
const handlePageChange = (page)=>{ const scrollToPage = (pageNumber)=>{
setPageInput(page);
};
const scrollToPage = (pageNumber) => {
pageNumber = _.clamp(pageNumber - 1, 0, totalPages - 1); pageNumber = _.clamp(pageNumber - 1, 0, totalPages - 1);
const iframe = document.getElementById('BrewRenderer'); const iframe = document.getElementById('BrewRenderer');
if(iframe && iframe.contentWindow) { if(iframe && iframe.contentWindow) {
@@ -111,21 +106,21 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button <button
id='zoom-to-fill' id='zoom-to-fill'
className='tool' className='tool'
onClick={()=>handleZoomChange(calculateZoom('fill'))} onClick={()=>handleZoomButton(calculateZoom('fill'))}
> >
<ZoomIcons.FitWidth title='Fit to Width' style={{ width: '1.5em' }} /> <ZoomIcons.FitWidth title='Fit to Width' style={{ width: '1.5em' }} />
</button> </button>
<button <button
id='zoom-to-fit' id='zoom-to-fit'
className='tool' className='tool'
onClick={()=>handleZoomChange(calculateZoom('fit'))} onClick={()=>handleZoomButton(calculateZoom('fit'))}
> >
<ZoomIcons.FitAll title='Zoom to Fit' style={{ width: '1.5em' }} /> <ZoomIcons.FitAll title='Zoom to Fit' style={{ width: '1.5em' }} />
</button> </button>
<button <button
id='zoom-out' id='zoom-out'
className='tool' className='tool'
onClick={()=>handleZoomChange(-20)} onClick={()=>handleZoomButton(-20)}
disabled={zoomLevel <= MIN_ZOOM} disabled={zoomLevel <= MIN_ZOOM}
> >
<i className='fas fa-magnifying-glass-minus' /> <i className='fas fa-magnifying-glass-minus' />
@@ -149,7 +144,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button <button
id='zoom-in' id='zoom-in'
className='tool' className='tool'
onClick={()=>handleZoomChange(20)} onClick={()=>handleZoomButton(20)}
disabled={zoomLevel >= MAX_ZOOM} disabled={zoomLevel >= MAX_ZOOM}
> >
<i className='fas fa-magnifying-glass-plus' /> <i className='fas fa-magnifying-glass-plus' />
@@ -170,8 +165,8 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button <button
id='previous-page' id='previous-page'
className='previousPage tool' className='previousPage tool'
onClick={()=>scrollToPage(pageInput - 1)} onClick={()=>scrollToPage(pageNum - 1)}
disabled={pageInput <= 1} disabled={pageNum <= 1}
> >
<i className='fas fa-arrow-left'></i> <i className='fas fa-arrow-left'></i>
</button> </button>
@@ -184,13 +179,11 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
name='page' name='page'
inputMode='numeric' inputMode='numeric'
pattern='[0-9]' pattern='[0-9]'
value={pageInput} value={pageNum}
onChange={(e)=>{ onChange={(e)=>{setPageNum(parseInt(e.target.value));}}
handlePageChange(e.target.value == false ? e.target.value : parseInt(e.target.value));}} onBlur={()=>scrollToPage(pageNum)}
onBlur={()=>scrollToPage(pageInput)} onKeyDown={(e)=>{e.key == 'Enter' ? scrollToPage(pageNum) : null;}}
onKeyDown={(e)=>{e.key == 'Enter' ? scrollToPage(pageInput) : null;}}
/> />
<span id='page-count'>/ {totalPages}</span> <span id='page-count'>/ {totalPages}</span>
</div> </div>
@@ -198,8 +191,8 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button <button
id='next-page' id='next-page'
className='tool' className='tool'
onClick={()=>scrollToPage(pageInput + 1)} onClick={()=>scrollToPage(pageNum + 1)}
disabled={pageInput >= totalPages} disabled={pageNum >= totalPages}
> >
<i className='fas fa-arrow-right'></i> <i className='fas fa-arrow-right'></i>
</button> </button>

View File

@@ -1,12 +1,12 @@
.toolBar { .toolBar {
position : absolute; position : absolute;
z-index : 1; z-index : 1;
box-sizing : border-box;
display : flex; display : flex;
flex-wrap : wrap; flex-wrap : wrap;
gap : 8px 30px; gap : 8px 30px;
align-items : center; align-items : center;
justify-content : space-around; justify-content : center;
box-sizing : border-box;
width : 100%; width : 100%;
height : auto; height : auto;
padding : 2px 0; padding : 2px 0;
@@ -15,17 +15,17 @@
background-color : #555555; background-color : #555555;
.group { .group {
box-sizing : border-box;
display : flex; display : flex;
gap: 0 3px; gap : 0 3px;
align-items : center; align-items : center;
justify-content : center; justify-content : center;
box-sizing : border-box; height : 28px;
height: 28px;
} }
.tool { .tool {
display: flex; display : flex;
align-items: center; align-items : center;
} }
input { input {
@@ -35,20 +35,20 @@
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
color : #000000; color : #000000;
background : #EEEEEE; background : #EEEEEE;
border : 1px solid gray; border : 1px solid gray;
&:focus { outline : 1px solid #d3d3d3; } &:focus { outline : 1px solid #D3D3D3; }
// `.range-input` if generic to all range inputs, or `#zoom-input` if only for zoom slider // `.range-input` if generic to all range inputs, or `#zoom-input` if only for zoom slider
&.range-input { &.range-input {
color: #D3D3D3; padding : 2px 0;
accent-color: #d3d3d3; color : #D3D3D3;
padding: 2px 0; accent-color : #D3D3D3;
&::-webkit-slider-thumb, &::-moz-slider-thumb { &::-webkit-slider-thumb, &::-moz-slider-thumb {
cursor : pointer; width : 5px;
width: 5px; height : 5px;
height: 5px; cursor : pointer;
outline: none; outline : none;
} }
&:hover::after { &:hover::after {
@@ -70,9 +70,9 @@
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input // `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
&#page-input { &#page-input {
width: 4ch; width : 4ch;
text-align: center; margin-right : 1ch;
margin-right: 1ch; text-align : center;
} }
@@ -80,21 +80,21 @@
button { button {
display : flex; box-sizing : content-box;
align-items : center; display : flex;
justify-content: center; align-items : center;
height : 100%; justify-content : center;
width : auto; width : auto;
min-width : 46px; min-width : 46px;
height : 100%;
padding : 0 0px; padding : 0 0px;
font-weight : unset; font-weight : unset;
color : inherit; color : inherit;
background-color : unset; background-color : unset;
box-sizing: content-box;
&:hover { background-color : #444444; } &:hover { background-color : #444444; }
&:focus { outline : 1px solid #d3d3d3; } &:focus { outline : 1px solid #D3D3D3; }
&:disabled { &:disabled {
color : #777777; color : #777777;

View File

@@ -86,7 +86,6 @@ const Editor = createClass({
handleControlKeys : function(e){ handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return; if(!(e.ctrlKey || e.metaKey)) return;
console.log(e);
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.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump();

View File

@@ -1,6 +1,7 @@
@import 'naturalcrit/styles/colors.less'; @import 'naturalcrit/styles/colors.less';
@navbarHeight : 28px; @navbarHeight : 28px;
@viewerToolsHeight : 32px;
@keyframes pinkColoring { @keyframes pinkColoring {
0% { color : pink; } 0% { color : pink; }

71
eslint.config.mjs Normal file
View File

@@ -0,0 +1,71 @@
import react from "eslint-plugin-react";
import jest from "eslint-plugin-jest";
import globals from "globals";
export default [{
ignores: ["build/"]
},
{
files : ['**/*.js', '**/*.jsx'],
plugins : { react, jest },
languageOptions : {
ecmaVersion : "latest",
sourceType : "module",
parserOptions : { ecmaFeatures: { jsx: true } },
globals : { ...globals.browser, ...globals.node }
},
rules: {
/** Errors **/
"camelcase" : ["error", { properties: "never" }],
"no-array-constructor" : "error",
"no-iterator" : "error",
"no-nested-ternary" : "error",
"no-new-object" : "error",
"no-proto" : "error",
"react/jsx-no-bind" : ["error", { allowArrowFunctions: true }],
"react/jsx-uses-react" : "error",
"react/prefer-es6-class" : ["error", "never"],
"jest/valid-expect" : ["error", { maxArgs: 3 }],
/** Warnings **/
"max-lines" : ["warn", { max: 200, skipComments: true, skipBlankLines: true }],
"max-depth" : ["warn", { max: 4 }],
"max-params" : ["warn", { max: 5 }],
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }],
"react/jsx-uses-vars" : "warn",
/** Fixable **/
"arrow-parens" : ["warn", "always"],
"brace-style" : ["warn", "1tbs", { allowSingleLine: true }],
"jsx-quotes" : ["warn", "prefer-single"],
"no-var" : "warn",
"prefer-const" : "warn",
"prefer-template" : "warn",
"quotes" : ["warn", "single", { allowTemplateLiterals: true }],
"semi" : ["warn", "always"],
/** Whitespace **/
"array-bracket-spacing" : ["warn", "never"],
"arrow-spacing" : ["warn", { before: false, after: false }],
"comma-spacing" : ["warn", { before: false, after: true }],
"indent" : ["warn", "tab", { MemberExpression: "off" }],
"linebreak-style" : "off",
"no-trailing-spaces" : "warn",
"no-whitespace-before-property" : "warn",
"object-curly-spacing" : ["warn", "always"],
"react/jsx-indent-props" : ["warn", "tab"],
"space-in-parens" : ["warn", "never"],
"template-curly-spacing" : ["warn", "never"],
"keyword-spacing" : ["warn", {
before : true,
after : true,
overrides : { if: { before: false, after: false } }
}],
"key-spacing" : ["warn", {
multiLine : { beforeColon: true, afterColon: true, align: "colon" },
singleLine : { beforeColon: false, afterColon: true }
}]
}
}
];

30891
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,8 @@
"quick": "node scripts/quick.js", "quick": "node scripts/quick.js",
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js", "build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
"builddev": "node scripts/buildHomebrew.js --dev", "builddev": "node scripts/buildHomebrew.js --dev",
"lint": "eslint --fix **/*.{js,jsx}", "lint": "eslint --fix",
"lint:dry": "eslint **/*.{js,jsx}", "lint:dry": "eslint",
"stylelint": "stylelint --fix **/*.{less}", "stylelint": "stylelint --fix **/*.{less}",
"stylelint:dry": "stylelint **/*.less", "stylelint:dry": "stylelint **/*.less",
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0", "circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
@@ -33,6 +33,7 @@
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace", "test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace", "test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace", "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace", "test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose", "test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js", "phb": "node scripts/phb.js",
@@ -84,8 +85,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-runtime": "^7.25.4",
"@babel/preset-env": "^7.25.3", "@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.13.0", "@googleapis/drive": "^8.13.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
@@ -124,14 +125,15 @@
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "@stylistic/stylelint-plugin": "^3.0.1",
"eslint": "^8.57.0", "eslint": "^9.9.0",
"eslint-plugin-jest": "^28.8.0", "eslint-plugin-jest": "^28.8.0",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.35.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",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.8.2", "stylelint": "^16.8.2",
"stylelint-config-recess-order": "^5.0.1", "stylelint-config-recess-order": "^5.1.0",
"stylelint-config-recommended": "^14.0.1", "stylelint-config-recommended": "^14.0.1",
"supertest": "^7.0.0" "supertest": "^7.0.0"
} }

View File

@@ -14,6 +14,7 @@ const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js'); const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename'); const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const templateFn = require('./../client/template.js');
const { DEFAULT_BREW } = require('./brewDefaults.js'); const { DEFAULT_BREW } = require('./brewDefaults.js');
@@ -420,8 +421,16 @@ if(isLocalEnvironment){
}); });
} }
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
}));
//Render the page //Render the page
const templateFn = require('./../client/template.js');
const renderPage = async (req, res)=>{ const renderPage = async (req, res)=>{
// Create configuration object // Create configuration object
const configuration = { const configuration = {
@@ -450,13 +459,6 @@ const renderPage = async (req, res)=>{
return page; return page;
}; };
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
}));
//v=====----- Error-Handling Middleware -----=====v// //v=====----- Error-Handling Middleware -----=====v//
//Format Errors as plain objects so all fields will appear in the string sent //Format Errors as plain objects so all fields will appear in the string sent
const formatErrors = (key, value)=>{ const formatErrors = (key, value)=>{

View File

@@ -3,7 +3,7 @@ const _ = require('lodash');
const Marked = require('marked'); const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables'); const MarkedExtendedTables = require('marked-extended-tables');
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite'); const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id'); const { gfmHeadingId: MarkedGFMHeadingId, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id');
const { markedEmoji: MarkedEmojis } = require('marked-emoji'); const { markedEmoji: MarkedEmojis } = require('marked-emoji');
//Icon fonts included so they can appear in emoji autosuggest dropdown //Icon fonts included so they can appear in emoji autosuggest dropdown
@@ -86,7 +86,7 @@ renderer.link = function (href, title, text) {
if(href[0] == '#') { if(href[0] == '#') {
self = true; self = true;
} }
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); href = cleanUrl(href);
if(href === null) { if(href === null) {
return text; return text;
@@ -356,6 +356,27 @@ const superSubScripts = {
} }
}; };
const forcedParagraphBreaks = {
name : 'hardBreaks',
level : 'block',
start(src) { return src.match(/\n:+$/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const regex = /^(:+)(?:\n|$)/ym;
const match = regex.exec(src);
if(match?.length) {
return {
type : 'hardBreaks', // Should match "name" above
raw : match[0], // Text to consume from the source
length : match[1].length,
text : ''
};
}
},
renderer(token) {
return `<div class='blank'></div>`.repeat(token.length).concat('\n');
}
};
const definitionListsSingleLine = { const definitionListsSingleLine = {
name : 'definitionListsSingleLine', name : 'definitionListsSingleLine',
level : 'block', level : 'block',
@@ -400,9 +421,9 @@ const definitionListsSingleLine = {
const definitionListsMultiLine = { const definitionListsMultiLine = {
name : 'definitionListsMultiLine', name : 'definitionListsMultiLine',
level : 'block', level : 'block',
start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y; const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
let match; let match;
let endIndex = 0; let endIndex = 0;
const definitions = []; const definitions = [];
@@ -707,33 +728,20 @@ const MarkedEmojiOptions = {
}; };
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock); Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
const nonWordAndColonTest = /[^\w:]/g; function cleanUrl(href) {
const cleanUrl = function (sanitize, base, href) { try {
if(sanitize) { href = encodeURI(href).replace(/%25/g, '%');
let prot; } catch {
try { return null;
prot = decodeURIComponent(unescape(href)) }
.replace(nonWordAndColonTest, '') return href;
.toLowerCase(); }
} catch (e) {
return null;
}
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
return null;
}
}
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
return null;
}
return href;
};
const escapeTest = /[&<>"']/; const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g; const escapeReplace = /[&<>"']/g;
@@ -828,13 +836,15 @@ let globalPageNumber = 0;
module.exports = { module.exports = {
marked : Marked, marked : Marked,
render : (rawBrewText, pageNumber=1)=>{ render : (rawBrewText, pageNumber=0)=>{
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
varsQueue = []; //Could move into MarkedVariables() varsQueue = []; //Could move into MarkedVariables()
globalPageNumber = pageNumber; globalPageNumber = pageNumber;
if(pageNumber==0) {
MarkedGFMResetHeadingIDs();
}
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`) rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
const opts = Marked.defaults; const opts = Marked.defaults;
rawBrewText = opts.hooks.preprocess(rawBrewText); rawBrewText = opts.hooks.preprocess(rawBrewText);

View File

@@ -88,4 +88,16 @@ describe('Multiline Definition Lists', ()=>{
const rendered = Markdown.render(source).trim(); const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
}); });
test('Multiline Definition Term must have at least one non-empty Definition', function() {
const source = 'Term 1\n::';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>`);
});
test('Multiline Definition List must have at least one non-newline character after ::', function() {
const source = 'Term 1\n::\nDefinition 1\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Definition 1</p>`);
});
}); });

View File

@@ -0,0 +1,47 @@
/* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js');
describe('Hard Breaks', ()=>{
test('Single Break', function() {
const source = ':\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>`);
});
test('Double Break', function() {
const source = '::\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div>`);
});
test('Triple Break', function() {
const source = ':::\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
});
test('Many Break', function() {
const source = '::::::::::\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
});
test('Multiple sets of Breaks', function() {
const source = ':::\n:::\n:::';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
});
test('Break directly between two paragraphs', function() {
const source = 'Line 1\n::\nLine 2';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Line 2</p>`);
});
test('Ignored inside a code block', function() {
const source = '```\n\n:\n\n```\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:\n</code></pre>`);
});
});

View File

@@ -236,7 +236,7 @@ body { counter-reset : page-numbers; }
left : 50%; left : 50%;
width : 50%; width : 50%;
height : 50%; height : 50%;
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))); transform : translateX(-50%) translateY(50%) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))) rotate(calc(-1deg * var(--rotation)));
} }
& img { & img {
position : absolute; position : absolute;