mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-08-06 08:57:38 +00:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -1,71 +1,72 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
|
|
||||||
const BrewCleanup = createReactClass({
|
const BrewCleanup = ({})=>{
|
||||||
displayName : 'BrewCleanup',
|
const [count, setCount] = useState(0);
|
||||||
getDefaultProps(){
|
const [pending, setPending] = useState(false);
|
||||||
return {};
|
const [primed, setPrimed] = useState(false);
|
||||||
},
|
const [error, setError] = useState(null);
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
count : 0,
|
|
||||||
|
|
||||||
pending : false,
|
const prime = async ()=>{
|
||||||
primed : false,
|
setPending(true);
|
||||||
err : null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
prime(){
|
|
||||||
this.setState({ pending: true });
|
|
||||||
|
|
||||||
request.get('/admin/cleanup')
|
try {
|
||||||
.then((res)=>this.setState({ count: res.body.count, primed: true }))
|
const res = await request.get('/admin/cleanup');
|
||||||
.catch((err)=>this.setState({ error: err }))
|
|
||||||
.finally(()=>this.setState({ pending: false }));
|
|
||||||
},
|
|
||||||
cleanup(){
|
|
||||||
this.setState({ pending: true });
|
|
||||||
|
|
||||||
request.post('/admin/cleanup')
|
setCount(res.body.count);
|
||||||
.then((res)=>this.setState({ count: res.body.count }))
|
setPrimed(true);
|
||||||
.catch((err)=>this.setState({ error: err }))
|
} catch (err) {
|
||||||
.finally(()=>this.setState({ pending: false, primed: false }));
|
setError(err);
|
||||||
},
|
} finally {
|
||||||
renderPrimed(){
|
setPending(false);
|
||||||
if(!this.state.primed) return;
|
|
||||||
|
|
||||||
if(!this.state.count){
|
|
||||||
return <div className='result noBrews'>No Matching Brews found.</div>;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = async ()=>{
|
||||||
|
setPending(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await request.post('/admin/cleanup');
|
||||||
|
|
||||||
|
setCount(res.body.count);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setPending(false);
|
||||||
|
setPrimed(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const renderPrimed = ()=>{
|
||||||
|
if(!primed) return;
|
||||||
|
|
||||||
|
if(!count) return <div className='result noBrews'>No Matching Brews found.</div>;
|
||||||
|
|
||||||
return <div className='result'>
|
return <div className='result'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={()=>cleanup()} className='remove'>
|
||||||
{this.state.pending
|
{pending
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: <span><i className='fas fa-times' /> Remove</span>
|
: <span><i className='fas fa-times' /> Remove</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<span>Found {this.state.count} Brews that could be removed. </span>
|
<span>Found {count} Brews that could be removed. </span>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
};
|
||||||
render(){
|
|
||||||
return <div className='brewUtil brewCleanup'>
|
return <div className='brewUtil brewCleanup'>
|
||||||
<h2> Brew Cleanup </h2>
|
<h2> Brew Cleanup </h2>
|
||||||
<p>Removes very short brews to tidy up the database</p>
|
<p>Removes very short brews to tidy up the database</p>
|
||||||
|
|
||||||
<button onClick={this.prime} className='query'>
|
<button onClick={()=>prime()} className='query'>
|
||||||
{this.state.pending
|
{pending
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: 'Query Brews'
|
: 'Query Brews'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
{this.renderPrimed()}
|
{renderPrimed()}
|
||||||
|
|
||||||
{this.state.error
|
{error && <div className='error noBrews'>{error.toString()}</div>}
|
||||||
&& <div className='error noBrews'>{this.state.error.toString()}</div>
|
|
||||||
}
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
|
||||||
});
|
};
|
||||||
|
|
||||||
export default BrewCleanup;
|
export default BrewCleanup;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint max-lines: ["error", { "max": 400 }] */
|
/* eslint max-lines: ["error", { "max": 405 }] */
|
||||||
import './codeEditor.less';
|
import './codeEditor.less';
|
||||||
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||||
|
|
||||||
@@ -10,21 +10,26 @@ import {
|
|||||||
highlightActiveLine,
|
highlightActiveLine,
|
||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
Decoration,
|
Decoration,
|
||||||
ViewPlugin,
|
|
||||||
drawSelection,
|
drawSelection,
|
||||||
dropCursor,
|
dropCursor,
|
||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
crosshairCursor,
|
crosshairCursor,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
|
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
|
||||||
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
|
import {
|
||||||
import { foldEffect } from '@codemirror/language';
|
unfoldAll as unfoldAllCmd,
|
||||||
|
foldGutter,
|
||||||
|
foldKeymap,
|
||||||
|
foldEffect,
|
||||||
|
foldState,
|
||||||
|
syntaxHighlighting,
|
||||||
|
} from '@codemirror/language';
|
||||||
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
||||||
import { languages } from '@codemirror/language-data';
|
import { languages } from '@codemirror/language-data';
|
||||||
import { css } from '@codemirror/lang-css';
|
import { css } from '@codemirror/lang-css';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { html } from '@codemirror/lang-html';
|
import { html } from '@codemirror/lang-html';
|
||||||
import { autocompleteEmoji } from './autocompleteEmoji.js';
|
import { autocompleteEmoji } from './extensions/autocompleteEmoji.js';
|
||||||
import { searchKeymap, search } from '@codemirror/search';
|
import { searchKeymap, search } from '@codemirror/search';
|
||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
|
|
||||||
@@ -38,71 +43,13 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
|||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
const highlightCompartment = new Compartment();
|
const highlightCompartment = new Compartment();
|
||||||
|
|
||||||
console.log(themes);
|
import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js';
|
||||||
|
import foldOnPages from './extensions/customFolding.js';
|
||||||
import { generalKeymap, markdownKeymap } from './customKeyMaps.js';
|
import { customHighlightPlugin, customHighlightStyle } from './extensions/customHighlight.js';
|
||||||
import foldOnPages from './customFolding.js';
|
import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js';
|
||||||
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
|
||||||
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js';
|
|
||||||
|
|
||||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
|
|
||||||
const createHighlightPlugin = (renderer, tab)=>{
|
|
||||||
//this function takes the custom tokens created in the tokenize function in customhighlight files
|
|
||||||
//takes the tokens defined by that function and assigns classes to them
|
|
||||||
//it also creates page number and snippet number widgets
|
|
||||||
|
|
||||||
let tokenize;
|
|
||||||
|
|
||||||
if(tab === 'brewStyles') {
|
|
||||||
tokenize = tokenizeCustomCSS;
|
|
||||||
} else {
|
|
||||||
tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
constructor(view) {
|
|
||||||
this.decorations = this.buildDecorations(view);
|
|
||||||
}
|
|
||||||
update(update) {
|
|
||||||
if(update.docChanged) {
|
|
||||||
this.decorations = this.buildDecorations(update.view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildDecorations(view) {
|
|
||||||
const decos = [];
|
|
||||||
const tokens = tokenize(view.state.doc.toString());
|
|
||||||
let pageCount = 1;
|
|
||||||
let snippetCount = 0;
|
|
||||||
|
|
||||||
tokens.forEach((tok)=>{
|
|
||||||
const line = view.state.doc.line(tok.line + 1);
|
|
||||||
|
|
||||||
if(tok.from != null && tok.to != null && tok.from < tok.to) {
|
|
||||||
decos.push(Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to));
|
|
||||||
} else {
|
|
||||||
decos.push(Decoration.line({ class: `cm-${tok.type}` }).range(line.from));
|
|
||||||
if(tok.type === 'pageLine' && tab === 'brewText') {
|
|
||||||
pageCount++;
|
|
||||||
line.from === 0 && pageCount--;
|
|
||||||
decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
|
|
||||||
}
|
|
||||||
if(tok.type === 'snippetLine' && tab === 'brewSnippets') {
|
|
||||||
snippetCount++;
|
|
||||||
decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
|
|
||||||
return Decoration.set(decos);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ decorations: (v)=>v.decorations }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setProgrammaticCursorLine = StateEffect.define();
|
const setProgrammaticCursorLine = StateEffect.define();
|
||||||
|
|
||||||
const programmaticCursorLineField = StateField.define({
|
const programmaticCursorLineField = StateField.define({
|
||||||
@@ -151,11 +98,14 @@ const CodeEditor = forwardRef(
|
|||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const viewRef = useRef(null);
|
const viewRef = useRef(null);
|
||||||
const docsRef = useRef({});
|
const docsRef = useRef({});
|
||||||
|
const tabRef = useRef(tab);
|
||||||
const prevTabRef = useRef(tab);
|
const prevTabRef = useRef(tab);
|
||||||
|
const scrollRef = useRef({});
|
||||||
|
const foldsRef = useRef({});
|
||||||
const pageMap = useRef([]);
|
const pageMap = useRef([]);
|
||||||
|
|
||||||
const recomputePages = (doc)=>{
|
const recomputePages = (doc)=>{
|
||||||
|
if(tab !== 'brewText') return;
|
||||||
const pages = [0];
|
const pages = [0];
|
||||||
const text = doc.toString();
|
const text = doc.toString();
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@@ -181,6 +131,14 @@ const CodeEditor = forwardRef(
|
|||||||
return page;
|
return page;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFoldRanges = (state)=>{
|
||||||
|
const folds = [];
|
||||||
|
state.field(foldState, false)?.between(0, state.doc.length, (from, to)=>{
|
||||||
|
folds.push({ from, to });
|
||||||
|
});
|
||||||
|
return folds;
|
||||||
|
};
|
||||||
|
|
||||||
const createExtensions = ({ onChange, language, editorTheme })=>{
|
const createExtensions = ({ onChange, language, editorTheme })=>{
|
||||||
const setEventListeners = EditorView.updateListener.of((update)=>{
|
const setEventListeners = EditorView.updateListener.of((update)=>{
|
||||||
if(update.docChanged) {
|
if(update.docChanged) {
|
||||||
@@ -198,8 +156,6 @@ const CodeEditor = forwardRef(
|
|||||||
? syntaxHighlighting(customHighlightStyle)
|
? syntaxHighlighting(customHighlightStyle)
|
||||||
: syntaxHighlighting(legacyCustomHighlightStyle);
|
: syntaxHighlighting(legacyCustomHighlightStyle);
|
||||||
|
|
||||||
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
|
|
||||||
|
|
||||||
const languageExtension = language === 'css' ? css() : [markdown({ base: markdownLanguage, codeLanguages: languages }), html({ autoCloseTags: true })];
|
const languageExtension = language === 'css' ? css() : [markdown({ base: markdownLanguage, codeLanguages: languages }), html({ autoCloseTags: true })];
|
||||||
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : themes[editorTheme] || themes['default'];
|
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : themes[editorTheme] || themes['default'];
|
||||||
|
|
||||||
@@ -222,7 +178,7 @@ const CodeEditor = forwardRef(
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
//highlights
|
//highlights
|
||||||
highlightCompartment.of([customHighlightPlugin, highlightExtension]),
|
highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]),
|
||||||
themeCompartment.of(themeExtension),
|
themeCompartment.of(themeExtension),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
@@ -267,11 +223,10 @@ const CodeEditor = forwardRef(
|
|||||||
ticking = true;
|
ticking = true;
|
||||||
requestAnimationFrame(()=>{
|
requestAnimationFrame(()=>{
|
||||||
const top = view.scrollDOM.scrollTop;
|
const top = view.scrollDOM.scrollTop;
|
||||||
|
scrollRef.current[tabRef.current] = top;
|
||||||
const block = view.lineBlockAtHeight(top);
|
const block = view.lineBlockAtHeight(top);
|
||||||
|
const page = findPageFromPos(block.from);
|
||||||
const page = findPageFromPos(block.from); // CHANGED
|
|
||||||
onViewChange(page);
|
onViewChange(page);
|
||||||
|
|
||||||
ticking = false;
|
ticking = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -286,12 +241,23 @@ const CodeEditor = forwardRef(
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const restoreFolds = (view, folds)=>{
|
||||||
|
if(!folds?.length) return;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
effects : folds.map((f)=>foldEffect.of(f))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
if(!view) return;
|
if(!view) return;
|
||||||
|
|
||||||
|
tabRef.current = tab;
|
||||||
const prevTab = prevTabRef.current;
|
const prevTab = prevTabRef.current;
|
||||||
|
|
||||||
|
foldsRef.current[prevTab] = getFoldRanges(view.state);
|
||||||
|
|
||||||
if(prevTab !== tab) {
|
if(prevTab !== tab) {
|
||||||
docsRef.current[prevTab] = view.state;
|
docsRef.current[prevTab] = view.state;
|
||||||
|
|
||||||
@@ -305,6 +271,16 @@ const CodeEditor = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.setState(nextState);
|
view.setState(nextState);
|
||||||
|
restoreFolds(view, foldsRef.current[tab]);
|
||||||
|
|
||||||
|
const savedScroll = scrollRef.current[tab];
|
||||||
|
|
||||||
|
if(savedScroll != null) {
|
||||||
|
requestAnimationFrame(()=>{
|
||||||
|
view.scrollDOM.scrollTop = savedScroll;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
prevTabRef.current = tab;
|
prevTabRef.current = tab;
|
||||||
}
|
}
|
||||||
view.focus();
|
view.focus();
|
||||||
@@ -343,10 +319,8 @@ const CodeEditor = forwardRef(
|
|||||||
? syntaxHighlighting(customHighlightStyle)
|
? syntaxHighlighting(customHighlightStyle)
|
||||||
: syntaxHighlighting(legacyCustomHighlightStyle);
|
: syntaxHighlighting(legacyCustomHighlightStyle);
|
||||||
|
|
||||||
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
|
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects : highlightCompartment.reconfigure([customHighlightPlugin, highlightExtension]),
|
effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab), highlightExtension]),
|
||||||
});
|
});
|
||||||
}, [renderer, tab]);
|
}, [renderer, tab]);
|
||||||
|
|
||||||
@@ -355,7 +329,6 @@ const CodeEditor = forwardRef(
|
|||||||
injectText : (text)=>{
|
injectText : (text)=>{
|
||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
|
|
||||||
|
|
||||||
view.dispatch(
|
view.dispatch(
|
||||||
view.state.replaceSelection(text)
|
view.state.replaceSelection(text)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,7 +63,8 @@
|
|||||||
&.term { color : rgb(96, 117, 143); }
|
&.term { color : rgb(96, 117, 143); }
|
||||||
&.definition { color : rgb(97, 57, 178); }
|
&.definition { color : rgb(97, 57, 178); }
|
||||||
}
|
}
|
||||||
.cm-block:not(.cm-comment) {
|
.cm-block:not(.cm-comment),
|
||||||
|
.cm-block:not(.cm-comment) * {
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
color : purple;
|
color : purple;
|
||||||
}
|
}
|
||||||
@@ -100,6 +101,10 @@
|
|||||||
vertical-align : sub;
|
vertical-align : sub;
|
||||||
color : rgb(123, 123, 15);
|
color : rgb(123, 123, 15);
|
||||||
}
|
}
|
||||||
|
.cm-strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-definitionList {
|
.cm-definitionList {
|
||||||
.cm-definitionTerm { color : rgb(96, 117, 143); }
|
.cm-definitionTerm { color : rgb(96, 117, 143); }
|
||||||
.cm-definitionColon:not(:has(.cm-comment)) {
|
.cm-definitionColon:not(:has(.cm-comment)) {
|
||||||
@@ -157,6 +162,33 @@
|
|||||||
outline : 1px inset #00000055 !important;
|
outline : 1px inset #00000055 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-image {
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
.cm-preview {
|
||||||
|
object-fit: contain;
|
||||||
|
position:absolute;
|
||||||
|
bottom:0;
|
||||||
|
left:0;
|
||||||
|
height:200px;
|
||||||
|
width:200px;
|
||||||
|
height:200px;
|
||||||
|
padding:5px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius:10px;
|
||||||
|
border:3px solid grey;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity:0;
|
||||||
|
transition:0.2s opacity 0.5s;
|
||||||
|
translate:0 100%;
|
||||||
|
z-index:1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .cm-preview {
|
||||||
|
opacity:1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Tab character visualization (optional) */
|
/* Tab character visualization (optional) */
|
||||||
//.cm-tab {
|
//.cm-tab {
|
||||||
// background: url(...) no-repeat right;
|
// background: url(...) no-repeat right;
|
||||||
|
|||||||
+186
-7
@@ -1,5 +1,15 @@
|
|||||||
|
/* eslint max-lines: ["error", { "max": 500 }] */
|
||||||
import { HighlightStyle } from '@codemirror/language';
|
import { HighlightStyle } from '@codemirror/language';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
|
import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight';
|
||||||
|
import {
|
||||||
|
Decoration,
|
||||||
|
ViewPlugin,
|
||||||
|
} from '@codemirror/view';
|
||||||
|
import {
|
||||||
|
syntaxTree,
|
||||||
|
ensureSyntaxTree
|
||||||
|
} from '@codemirror/language';
|
||||||
|
|
||||||
// Making the tokens
|
// Making the tokens
|
||||||
const customTags = {
|
const customTags = {
|
||||||
@@ -16,13 +26,14 @@ const customTags = {
|
|||||||
definitionTerm : 'definitionTerm', // .cm-definitionTerm
|
definitionTerm : 'definitionTerm', // .cm-definitionTerm
|
||||||
definitionDesc : 'definitionDesc', // .cm-definitionDesc
|
definitionDesc : 'definitionDesc', // .cm-definitionDesc
|
||||||
definitionColon : 'definitionColon', // .cm-definitionColon
|
definitionColon : 'definitionColon', // .cm-definitionColon
|
||||||
|
strikethrough : 'strikethrough', // .cm-strikethrough
|
||||||
|
|
||||||
//CSS
|
//CSS
|
||||||
|
|
||||||
variable : 'variable',
|
variable : 'variable',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function tokenizeCustomMarkdown(text) {
|
function tokenizeCustomMarkdown(text) {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
|
|
||||||
@@ -81,6 +92,23 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Strikethrough ---
|
||||||
|
if(/\~/.test(lineText)) {
|
||||||
|
const strikethroughRegex = /~(?!\s)(.+?)(?<!\s)~/g;
|
||||||
|
|
||||||
|
const match = strikethroughRegex.exec(lineText);
|
||||||
|
const type = customTags.strikethrough;
|
||||||
|
|
||||||
|
if(match) {
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
type,
|
||||||
|
from : match.index,
|
||||||
|
to : match.index + match[0].length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- single line def list ---
|
// --- single line def list ---
|
||||||
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
|
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
|
||||||
const match = singleLineRegex.exec(lineText);
|
const match = singleLineRegex.exec(lineText);
|
||||||
@@ -131,7 +159,6 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
|
if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
|
||||||
const startLine = lineNumber;
|
const startLine = lineNumber;
|
||||||
const defs = [];
|
const defs = [];
|
||||||
let endLine = startLine;
|
|
||||||
|
|
||||||
// collect all following :: definitions
|
// collect all following :: definitions
|
||||||
for (let i = lineNumber + 1; i < lines.length; i++) {
|
for (let i = lineNumber + 1; i < lines.length; i++) {
|
||||||
@@ -140,7 +167,6 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
const defMatch = /^(::)(.+)$/.exec(nextLine);
|
const defMatch = /^(::)(.+)$/.exec(nextLine);
|
||||||
if(!onlyColonsMatch && defMatch) {
|
if(!onlyColonsMatch && defMatch) {
|
||||||
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
|
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
|
||||||
endLine = i;
|
|
||||||
} else break;
|
} else break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,13 +208,14 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(lineText.includes('{') && lineText.includes('}')) {
|
if(lineText.includes('{') && lineText.includes('}')) {
|
||||||
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
|
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = injectionRegex.exec(lineText)) !== null) {
|
while ((match = injectionRegex.exec(lineText)) !== null) {
|
||||||
tokens.push({
|
tokens.push({
|
||||||
line : lineNumber,
|
line : lineNumber,
|
||||||
from : match.index,
|
from : match.indices[1][0],
|
||||||
to : match.index + match[1].length,
|
to : match.indices[1][1],
|
||||||
type : customTags.injection,
|
type : customTags.injection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,14 +250,21 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/,
|
/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/,
|
||||||
);
|
);
|
||||||
if(match) endCh = match.index + match[0].length;
|
if(match) endCh = match.index + match[0].length;
|
||||||
|
const closingMatch = lineText.match(/ *(}})/d);
|
||||||
|
|
||||||
|
if(closingMatch) {
|
||||||
|
tokens.push({ line: lineNumber, from: closingMatch.indices[1][0], to: closingMatch.indices[1][1], type: customTags.block });
|
||||||
|
} else {
|
||||||
tokens.push({ line: lineNumber, type: customTags.block });
|
tokens.push({ line: lineNumber, type: customTags.block });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tokenizeCustomCSS(text) {
|
function tokenizeCustomCSS(text) {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
|
|
||||||
@@ -289,5 +323,150 @@ export const customHighlightStyle = HighlightStyle.define([
|
|||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function getUrl(node, doc) {
|
||||||
|
let url = null;
|
||||||
|
|
||||||
|
const cursor = node.node.cursor();
|
||||||
|
|
||||||
|
if(cursor.firstChild()) {
|
||||||
|
do {
|
||||||
|
if(cursor.name === 'URL') {
|
||||||
|
url = doc.sliceString(cursor.from, cursor.to);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (cursor.nextSibling());
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { WidgetType } from '@codemirror/view';
|
||||||
|
|
||||||
|
class ImageWidget extends WidgetType {
|
||||||
|
constructor(url) {
|
||||||
|
super();
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.loading = "lazy";
|
||||||
|
img.className = 'cm-preview';
|
||||||
|
img.src = this.url;
|
||||||
|
|
||||||
|
|
||||||
|
img.onerror = ()=>{
|
||||||
|
img.src = 'client/icons/broken-image.jpg';
|
||||||
|
};
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other) {
|
||||||
|
return other.url === this.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function customHighlightPlugin(renderer, tab) {
|
||||||
|
//this function takes the custom tokens created in the tokenize function in customhighlight files
|
||||||
|
//takes the tokens defined by that function and assigns classes to them
|
||||||
|
//it also creates page number and snippet number widgets
|
||||||
|
|
||||||
|
let tokenize;
|
||||||
|
|
||||||
|
if(tab === 'brewStyles') {
|
||||||
|
tokenize = tokenizeCustomCSS;
|
||||||
|
} else {
|
||||||
|
tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
constructor(view) {
|
||||||
|
this.decorations = this.buildDecorations(view);
|
||||||
|
}
|
||||||
|
update(update) {
|
||||||
|
if(update.docChanged) {
|
||||||
|
this.decorations = this.buildDecorations(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildDecorations(view) {
|
||||||
|
const decos = [];
|
||||||
|
const tokens = tokenize(view.state.doc.toString());
|
||||||
|
let pageCount = 1;
|
||||||
|
let snippetCount = 0;
|
||||||
|
|
||||||
|
const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state);
|
||||||
|
tree.iterate({
|
||||||
|
enter : (node)=>{
|
||||||
|
if(node.name === 'Image') {
|
||||||
|
const url = getUrl(node, view.state.doc);
|
||||||
|
|
||||||
|
const widgetPosition = node.node.lastChild.from;
|
||||||
|
//this is not exactly standard, but should hold,
|
||||||
|
//and is the shortest way i could find of positioning
|
||||||
|
//the image inside the cm-image node
|
||||||
|
|
||||||
|
if(!url) return;
|
||||||
|
|
||||||
|
decos.push(
|
||||||
|
Decoration.mark({
|
||||||
|
class : 'cm-image'
|
||||||
|
}).range(node.from, node.to)
|
||||||
|
);
|
||||||
|
decos.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget : new ImageWidget(url),
|
||||||
|
side : 1
|
||||||
|
}).range(widgetPosition)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokens.forEach((token)=>{
|
||||||
|
const line = view.state.doc.line(token.line + 1);
|
||||||
|
|
||||||
|
if(token.from != null && token.to != null && token.from < token.to) {
|
||||||
|
const from = line.from + token.from;
|
||||||
|
const to = line.from + token.to;
|
||||||
|
|
||||||
|
const attrs = {};
|
||||||
|
if(token.type === 'Image' && token.url) {
|
||||||
|
|
||||||
|
attrs['data-url'] = token.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
decos.push(
|
||||||
|
Decoration.mark({
|
||||||
|
class : `cm-${token.type}`,
|
||||||
|
...(Object.keys(attrs).length
|
||||||
|
? { attributes: attrs }
|
||||||
|
: {})
|
||||||
|
}).range(from, to)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
decos.push(
|
||||||
|
Decoration.line({
|
||||||
|
class : `cm-${token.type}`
|
||||||
|
}).range(line.from)
|
||||||
|
);
|
||||||
|
if(token.type === 'pageLine' && tab === 'brewText') {
|
||||||
|
pageCount++;
|
||||||
|
if(line.from === 0) pageCount--;
|
||||||
|
decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
|
||||||
|
}
|
||||||
|
if(token.type === 'snippetLine' && tab === 'brewSnippets') {
|
||||||
|
snippetCount++;
|
||||||
|
decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
|
||||||
|
return Decoration.set(decos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ decorations: (v)=>v.decorations }
|
||||||
|
);
|
||||||
|
};
|
||||||
+22
-15
@@ -3,6 +3,17 @@ import { keymap } from '@codemirror/view';
|
|||||||
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
|
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
|
||||||
import { Prec } from '@codemirror/state';
|
import { Prec } from '@codemirror/state';
|
||||||
|
|
||||||
|
const insertTab = (view)=>{
|
||||||
|
const { from, to } = view.state.selection.main;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes : { from, to, insert: ' ' },
|
||||||
|
selection : { anchor: from + 2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const indentLess = (view)=>{
|
const indentLess = (view)=>{
|
||||||
const { from, to } = view.state.selection.main;
|
const { from, to } = view.state.selection.main;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
@@ -18,27 +29,23 @@ const indentLess = (view)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const wrapSelection = (prefix, suffix)=>(view)=>{
|
const wrapSelection = (prefix, suffix)=>(view)=>{
|
||||||
const { from, to } = view.state.selection.main;
|
const changes = [];
|
||||||
|
|
||||||
|
for (const range of view.state.selection.ranges) {
|
||||||
|
const { from, to } = range;
|
||||||
const selected = view.state.doc.sliceString(from, to);
|
const selected = view.state.doc.sliceString(from, to);
|
||||||
|
|
||||||
let text, selection;
|
let text;
|
||||||
|
|
||||||
if(from === to) {
|
if(from === to) { text = prefix + suffix; } else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
|
||||||
text = prefix + suffix;
|
|
||||||
selection = { anchor: from + prefix.length, head: from + prefix.length };
|
|
||||||
}
|
|
||||||
else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
|
|
||||||
text = selected.slice(prefix.length, -suffix.length);
|
text = selected.slice(prefix.length, -suffix.length);
|
||||||
selection = { anchor: from, head: from + text.length };
|
} else {text = `${prefix}${selected}${suffix}`;}
|
||||||
}
|
|
||||||
else {
|
changes.push({ from, to, insert: text });
|
||||||
text = `${prefix}${selected}${suffix}`;
|
|
||||||
selection = { anchor: from, head: from + text.length };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes : { from, to, insert: text },
|
changes
|
||||||
selection
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -162,7 +169,7 @@ const newPage = (view)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generalKeymap = Prec.high(keymap.of([
|
export const generalKeymap = Prec.high(keymap.of([
|
||||||
{ key: 'Tab', run: indentMore },
|
{ key: 'Tab', run: insertTab },
|
||||||
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
|
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
|
||||||
{ key: 'Mod-Shift-z', run: redo },
|
{ key: 'Mod-Shift-z', run: redo },
|
||||||
{ key: 'Mod-y', run: redo },
|
{ key: 'Mod-y', run: redo },
|
||||||
@@ -29,7 +29,6 @@ const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
|
|||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
|
||||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||||
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
|
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
|
||||||
<link href="${headerNavStylesUrl}" rel="stylesheet" />
|
<link href="${headerNavStylesUrl}" rel="stylesheet" />
|
||||||
@@ -42,6 +41,7 @@ const BrewPage = (props)=>{
|
|||||||
props = {
|
props = {
|
||||||
contents : '',
|
contents : '',
|
||||||
index : 0,
|
index : 0,
|
||||||
|
hoisted : false,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
const pageRef = useRef(null);
|
const pageRef = useRef(null);
|
||||||
@@ -221,7 +221,8 @@ const BrewRenderer = (props)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPages = ()=>{
|
const renderPages = (checkHoists = false)=>{
|
||||||
|
|
||||||
if(props.errors && props.errors.length)
|
if(props.errors && props.errors.length)
|
||||||
return renderedPages;
|
return renderedPages;
|
||||||
|
|
||||||
@@ -233,10 +234,16 @@ const BrewRenderer = (props)=>{
|
|||||||
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
||||||
|
|
||||||
_.forEach(rawPages, (page, index)=>{
|
_.forEach(rawPages, (page, index)=>{
|
||||||
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
const varsOnPageRegex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Find out if there are any vars on the page.
|
||||||
|
const forceRender = checkHoists &&
|
||||||
|
!props.hoisted &&
|
||||||
|
(page.match(varsOnPageRegex)); // forceRender forces pages outside of the PPR range to render if true.
|
||||||
|
// This is necessary on the first load to fully populate the variable table.
|
||||||
|
if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){
|
||||||
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
|
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if(!props.hoisted) { props.hoisted = true; } // Only fully hoist once.
|
||||||
return renderedPages;
|
return renderedPages;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,7 +283,7 @@ const BrewRenderer = (props)=>{
|
|||||||
window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash));
|
window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash));
|
||||||
|
|
||||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
renderPages(); //Make sure page is renderable before showing
|
renderPages(true); //Make sure page is renderable before showing
|
||||||
setState((prevState)=>({
|
setState((prevState)=>({
|
||||||
...prevState,
|
...prevState,
|
||||||
isMounted : true,
|
isMounted : true,
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ import cm5Themes from 'codemirror-5-themes';
|
|||||||
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
||||||
|
|
||||||
const themeNames = Object.entries(themes)
|
const themeNames = Object.entries(themes)
|
||||||
.filter(([name, value]) =>
|
.filter(([name, value])=>Array.isArray(value) &&
|
||||||
Array.isArray(value) &&
|
|
||||||
!name.endsWith('Init') &&
|
!name.endsWith('Init') &&
|
||||||
!name.endsWith('Style')
|
!name.endsWith('Style')
|
||||||
)
|
)
|
||||||
@@ -40,7 +39,7 @@ const themeNames = Object.entries(themes)
|
|||||||
const EditorThemes = [
|
const EditorThemes = [
|
||||||
'default',
|
'default',
|
||||||
...themeNames
|
...themeNames
|
||||||
.filter(name => name !== 'default')
|
.filter((name)=>name !== 'default')
|
||||||
.sort((a, b)=>a.localeCompare(b))
|
.sort((a, b)=>a.localeCompare(b))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Nav from './nav.jsx';
|
import Nav from './nav.jsx';
|
||||||
import { printCurrentBrew } from '@shared/helpers.js';
|
import { printCurrentBrew } from '@shared/helpers.js';
|
||||||
|
|
||||||
export default function(){
|
export default function(){
|
||||||
|
const [printing, setPrinting] = useState(false);
|
||||||
|
|
||||||
|
// listen for print cycle events to display "loading" message since it can take some time.
|
||||||
|
useEffect(()=>{
|
||||||
|
document.addEventListener('print:startprep', handlePrintStartPrep);
|
||||||
|
document.addEventListener('print:finishedprep', handlePrintPrepFinished);
|
||||||
|
return ()=>{
|
||||||
|
document.removeEventListener('print:startprep', handlePrintStartPrep);
|
||||||
|
document.removeEventListener('print:finishedprep', handlePrintPrepFinished);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePrintStartPrep = ()=>{ setPrinting(true); };
|
||||||
|
|
||||||
|
const handlePrintPrepFinished = ()=>{ setPrinting(false); };
|
||||||
|
|
||||||
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
|
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
|
||||||
get PDF
|
{printing ? 'loading' : 'get PDF'}
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const EditPage = (props)=>{
|
|||||||
|
|
||||||
const handleControlKeys = (e)=>{
|
const handleControlKeys = (e)=>{
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
if(e.keyCode === 83) trySaveRef.current(true);
|
if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle);
|
||||||
if(e.keyCode === 80) printCurrentBrew();
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
if([83, 80].includes(e.keyCode)) {
|
if([83, 80].includes(e.keyCode)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -118,13 +118,9 @@ const EditPage = (props)=>{
|
|||||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||||
setUnsavedChanges(hasChange);
|
setUnsavedChanges(hasChange);
|
||||||
|
|
||||||
if(autoSaveEnabled) trySave(false, hasChange);
|
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
|
||||||
}, [currentBrew]);
|
}, [currentBrew]);
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
trySave(true);
|
|
||||||
}, [saveGoogle]);
|
|
||||||
|
|
||||||
const handleSplitMove = ()=>{
|
const handleSplitMove = ()=>{
|
||||||
editorRef.current?.update();
|
editorRef.current?.update();
|
||||||
};
|
};
|
||||||
@@ -183,11 +179,13 @@ const EditPage = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleGoogleStorage = ()=>{
|
const toggleGoogleStorage = ()=>{
|
||||||
|
const newSaveGoogle = !saveGoogle;
|
||||||
setSaveGoogle((prev)=>!prev);
|
setSaveGoogle((prev)=>!prev);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
trySave(true, true, newSaveGoogle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const trySave = (immediate = false, hasChanges = true)=>{
|
const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
|
||||||
clearTimeout(saveTimeout.current);
|
clearTimeout(saveTimeout.current);
|
||||||
if(isSaving) return;
|
if(isSaving) return;
|
||||||
if(!hasChanges && !immediate) return;
|
if(!hasChanges && !immediate) return;
|
||||||
@@ -196,7 +194,7 @@ const EditPage = (props)=>{
|
|||||||
saveTimeout.current = setTimeout(async ()=>{
|
saveTimeout.current = setTimeout(async ()=>{
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
await save(currentBrew, saveGoogle)
|
await save(currentBrew, saveToGoogle)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
setError(err);
|
setError(err);
|
||||||
});
|
});
|
||||||
@@ -216,7 +214,7 @@ const EditPage = (props)=>{
|
|||||||
const brewToSave = {
|
const brewToSave = {
|
||||||
...brew,
|
...brew,
|
||||||
text : brew.text.normalize('NFC'),
|
text : brew.text.normalize('NFC'),
|
||||||
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
|
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1,
|
||||||
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
||||||
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
|
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
|
||||||
textBin : undefined,
|
textBin : undefined,
|
||||||
@@ -314,7 +312,7 @@ const EditPage = (props)=>{
|
|||||||
|
|
||||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||||
if(unsavedChanges)
|
if(unsavedChanges)
|
||||||
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
return <Nav.item className='save' onClick={()=>trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||||
|
|
||||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||||
if(autoSaveEnabled)
|
if(autoSaveEnabled)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ const NewPage = (props)=>{
|
|||||||
const updatedBrew = { ...currentBrew };
|
const updatedBrew = { ...currentBrew };
|
||||||
splitTextStyleAndMetadata(updatedBrew);
|
splitTextStyleAndMetadata(updatedBrew);
|
||||||
|
|
||||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm;
|
||||||
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -5,10 +5,6 @@
|
|||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
<link
|
|
||||||
href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700"
|
|
||||||
rel="stylesheet"
|
|
||||||
type="text/css" />
|
|
||||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
|
|||||||
Generated
+712
-695
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -88,10 +88,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.29.2",
|
"@babel/preset-env": "^7.29.5",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@babel/runtime": "^7.29.2",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@codemirror/autocomplete": "^6.20.1",
|
"@codemirror/autocomplete": "^6.20.2",
|
||||||
"@codemirror/commands": "^6.10.3",
|
"@codemirror/commands": "^6.10.3",
|
||||||
"@codemirror/highlight": "^0.19.8",
|
"@codemirror/highlight": "^0.19.8",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"@codemirror/language-data": "^6.5.2",
|
"@codemirror/language-data": "^6.5.2",
|
||||||
"@codemirror/search": "^6.6.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
"@codemirror/state": "^6.6.0",
|
"@codemirror/state": "^6.6.0",
|
||||||
"@codemirror/view": "^6.40.0",
|
"@codemirror/view": "^6.43.0",
|
||||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||||
"@googleapis/drive": "^20.1.0",
|
"@googleapis/drive": "^20.1.0",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
@@ -117,9 +117,9 @@
|
|||||||
"dedent": "^1.7.1",
|
"dedent": "^1.7.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "3.0.0",
|
"express-static-gzip": "3.0.1",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.5",
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
@@ -138,31 +138,31 @@
|
|||||||
"marked-variables": "^1.0.5",
|
"marked-variables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^9.3.3",
|
"mongoose": "^9.6.2",
|
||||||
"nanoid": "5.1.7",
|
"nanoid": "5.1.11",
|
||||||
"nconf": "^0.13.0",
|
"nconf": "^0.13.0",
|
||||||
"node": "^25.9.0",
|
"node": "^25.9.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.6",
|
||||||
"react-frame-component": "^5.3.2",
|
"react-frame-component": "^5.3.2",
|
||||||
"react-router": "^7.14.0",
|
"react-router": "^7.15.1",
|
||||||
"sanitize-filename": "1.6.4",
|
"sanitize-filename": "1.6.4",
|
||||||
"superagent": "^10.2.1"
|
"superagent": "^10.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^5.0.1",
|
"@stylistic/stylelint-plugin": "^5.0.1",
|
||||||
"babel-jest": "^30.3.0",
|
"babel-jest": "^30.4.1",
|
||||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||||
"eslint": "9.7",
|
"eslint": "9.7",
|
||||||
"eslint-plugin-jest": "^29.15.1",
|
"eslint-plugin-jest": "^29.15.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.3.0",
|
"jest": "^30.4.2",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^17.6.0",
|
"stylelint": "^17.11.1",
|
||||||
"stylelint-config-recess-order": "^7.7.0",
|
"stylelint-config-recess-order": "^7.7.0",
|
||||||
"stylelint-config-recommended": "^18.0.0",
|
"stylelint-config-recommended": "^18.0.0",
|
||||||
"supertest": "^7.1.4",
|
"supertest": "^7.1.4",
|
||||||
|
|||||||
+1
-1
@@ -593,7 +593,7 @@ export default async function createApp(vite) {
|
|||||||
|
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
'<head>',
|
'<head>',
|
||||||
`<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`
|
()=>{ return `<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`; }
|
||||||
);
|
);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
+10
-3
@@ -45,7 +45,7 @@ const migrateSystemsToTags = (brew) => {
|
|||||||
'3.5e' : 'system:D&D 3.5e',
|
'3.5e' : 'system:D&D 3.5e',
|
||||||
'Pathfinder' : 'system:Pathfinder 2e'
|
'Pathfinder' : 'system:Pathfinder 2e'
|
||||||
};
|
};
|
||||||
const systemTags = brew.systems.map(s => systemMap[s]);
|
const systemTags = brew.systems.map((s)=>systemMap[s]);
|
||||||
brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
|
brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
|
||||||
|
|
||||||
brew.systems = undefined;
|
brew.systems = undefined;
|
||||||
@@ -397,17 +397,24 @@ const api = {
|
|||||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result = [];
|
||||||
try {
|
try {
|
||||||
const patches = parsePatch(brewFromClient.patches);
|
const patches = parsePatch(brewFromClient.patches);
|
||||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||||
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
result = applyPatches(patches, encodeURI(brewFromServer.text));
|
||||||
if(patchedResult != brewFromClient.text)
|
const failedPatches = patches.map((patch, index)=>{if(!result[1][index]){ return patch; }});
|
||||||
|
if(failedPatches > 0){
|
||||||
|
throw (`Patch failure: ${failedPatches}/${result[1].length} did not apply`);
|
||||||
|
}
|
||||||
|
if(decodeURI(result[0]) != brewFromClient.text){
|
||||||
throw ('Patches did not apply cleanly, text mismatch detected');
|
throw ('Patches did not apply cleanly, text mismatch detected');
|
||||||
|
}
|
||||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||||
console.error('Failed to apply patches:', {
|
console.error('Failed to apply patches:', {
|
||||||
// patches : brewFromClient.patches,
|
// patches : brewFromClient.patches,
|
||||||
|
// result : result,
|
||||||
brewId : brewFromClient.editId || 'unknown',
|
brewId : brewFromClient.editId || 'unknown',
|
||||||
error : err
|
error : err
|
||||||
});
|
});
|
||||||
|
|||||||
+51
-4
@@ -105,14 +105,35 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
|
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const printCurrentBrew = ()=>{
|
const printCurrentBrew = async ()=>{
|
||||||
if(window.typeof !== 'undefined') {
|
if(window.typeof !== 'undefined') {
|
||||||
|
// fire a custom event for the print cycle
|
||||||
|
document.dispatchEvent(new CustomEvent('print:startprep'));
|
||||||
|
try {
|
||||||
|
const iframeDoc = window.frames['BrewRenderer'].contentDocument;
|
||||||
|
|
||||||
|
// get all img elements with lazy loading (currently only elements generated through MarkedJS)
|
||||||
|
const lazyImages = [...iframeDoc.querySelectorAll('img[loading="lazy"]')];
|
||||||
|
lazyImages.forEach((img)=>{ img.loading = 'eager'; });
|
||||||
|
|
||||||
|
// waits for images to load before resolving promise and opening print dialog
|
||||||
|
await Promise.all(
|
||||||
|
lazyImages
|
||||||
|
.filter((img)=>!img.complete)
|
||||||
|
.map((img)=>new Promise((resolve)=>{ img.onload = resolve; img.onerror = resolve; }))
|
||||||
|
);
|
||||||
|
|
||||||
window.frames['BrewRenderer'].contentWindow.print();
|
window.frames['BrewRenderer'].contentWindow.print();
|
||||||
|
|
||||||
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
|
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
|
||||||
const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0);
|
const node = iframeDoc.getElementsByClassName('brewRenderer').item(0);
|
||||||
node.style.display='none';
|
node.style.display='none';
|
||||||
node.offsetHeight; // accessing this is enough to trigger a reflow
|
node.offsetHeight; // accessing this is enough to trigger a reflow
|
||||||
node.style.display='';
|
node.style.display='';
|
||||||
|
} finally {
|
||||||
|
// when lazy load images have all been loaded, and the doc re-rendered for print preview, emit 'finished' event.
|
||||||
|
document.dispatchEvent(new CustomEvent('print:finishedprep'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,9 +181,35 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
|
|||||||
// Char-level diff
|
// Char-level diff
|
||||||
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
||||||
if(clientText[i] !== serverText[i]) {
|
if(clientText[i] !== serverText[i]) {
|
||||||
|
const getMismatchContext = (text, index, name, size = 10)=>{
|
||||||
|
const lower = Math.max(index - size, 0);
|
||||||
|
const upper = Math.min(index + size, text.length);
|
||||||
|
const slice = `${JSON.stringify(text.slice(lower, index)).slice(1, -1)}\u001B[31m${JSON.stringify(text[i]).slice(1, -1)}\u001B[0m${JSON.stringify(text.slice(index+1, upper)).slice(1, -1)}`;
|
||||||
|
const lineNo = text.slice(0, index).split('\n').length;
|
||||||
|
const code = `U+${text.charCodeAt(i).toString(16).toUpperCase()}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
lineNo,
|
||||||
|
code,
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
slice
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const boundSize = 10;
|
||||||
|
|
||||||
|
const clientContext = getMismatchContext(clientText, i, 'Client', boundSize);
|
||||||
|
const serverContext = getMismatchContext(serverText, i, 'Server', boundSize);
|
||||||
|
|
||||||
|
const logContext = (context)=>{
|
||||||
|
console.log(` ${context.name} - line ${context.lineNo} : (${context.code})\t${context.slice}`);
|
||||||
|
};
|
||||||
|
|
||||||
console.log(`Char mismatch at index ${i}:`);
|
console.log(`Char mismatch at index ${i}:`);
|
||||||
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
logContext(clientContext);
|
||||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
logContext(serverContext);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ renderer.image = function (token) {
|
|||||||
if(href === null)
|
if(href === null)
|
||||||
return text;
|
return text;
|
||||||
|
|
||||||
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
|
let out = `<img loading="lazy" src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
|
||||||
if(title)
|
if(title)
|
||||||
out += ` title="${title}"`;
|
out += ` title="${title}"`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,7 @@
|
|||||||
@import './animations.less';
|
@import './animations.less';
|
||||||
@import './colors.less';
|
@import './colors.less';
|
||||||
@import './tooltip.less';
|
@import './tooltip.less';
|
||||||
@font-face {
|
@import './fonts/fonts.css';
|
||||||
font-family : 'CodeLight';
|
|
||||||
src : url('./CODE Light.otf') format('opentype');
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family : 'CodeBold';
|
|
||||||
src : url('./CODE Bold.otf') format('opentype');
|
|
||||||
}
|
|
||||||
html,body, #reactRoot {
|
html,body, #reactRoot {
|
||||||
height : 100vh;
|
height : 100vh;
|
||||||
min-height : 100vh;
|
min-height : 100vh;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* open-sans-latin-wght-normal */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-style : normal;
|
||||||
|
font-weight : normal;
|
||||||
|
src : url('open-sans-latin-400-normal.woff2') format('woff2');
|
||||||
|
font-display : swap;
|
||||||
|
unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nowhere is font-weight: 700 actually used with Open Sans, everything is set to 800.
|
||||||
|
* But, 800 *is* too bold. And since we don't have an 800 font file, it's just using the
|
||||||
|
* 700 font file and it looks fine. Not sure it's worth changing everything to 700?
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-style : normal;
|
||||||
|
font-weight : bold;
|
||||||
|
src : url('open-sans-latin-700-normal.woff2') format('woff2');
|
||||||
|
font-display : swap;
|
||||||
|
unicode-range : U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family : 'CodeLight';
|
||||||
|
font-style : normal;
|
||||||
|
src : url('./CODE Light.otf') format('opentype');
|
||||||
|
font-display : block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family : 'CodeBold';
|
||||||
|
font-style : normal;
|
||||||
|
src : url('./CODE Bold.otf') format('opentype');
|
||||||
|
font-display : block;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -324,7 +324,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
it('Renders an image element with injected style', function() {
|
it('Renders an image element with injected style', function() {
|
||||||
const source = '{position:absolute}';
|
const source = '{position:absolute}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute;" src="https://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute;" loading="lazy" src="https://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders an element modified by only the first of two consecutive injections', function() {
|
it('Renders an element modified by only the first of two consecutive injections', function() {
|
||||||
@@ -343,19 +343,19 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
it('Renders an image with added attributes', function() {
|
it('Renders an image with added attributes', function() {
|
||||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" loading="lazy" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders an image with "=" in the url, and added attributes', function() {
|
it('Renders an image with "=" in the url, and added attributes', function() {
|
||||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png?auth=12345&height=1024); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png?auth=12345&height=1024); position:absolute; bottom:20px; left:130px; width:220px;" loading="lazy" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders an image and added attributes with "=" in the value, ', function() {
|
it('Renders an image and added attributes with "=" in the value, ', function() {
|
||||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`;
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" loading="lazy" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -315,21 +315,21 @@ describe('Normal Links and Images', ()=>{
|
|||||||
const source = ``;
|
const source = ``;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
<p><img src="url" alt="alt text" style="--HB_src:url(url);"></p>`.trimReturns());
|
<p><img loading="lazy" src="url" alt="alt text" style="--HB_src:url(url);"></p>`.trimReturns());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders normal images with a title', function() {
|
it('Renders normal images with a title', function() {
|
||||||
const source = 'An image !';
|
const source = 'An image !';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
<p>An image <img src="url" alt="alt text" style="--HB_src:url(url);" title="and title">!</p>`.trimReturns());
|
<p>An image <img loading="lazy" src="url" alt="alt text" style="--HB_src:url(url);" title="and title">!</p>`.trimReturns());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Applies curly injectors to images', function() {
|
it('Applies curly injectors to images', function() {
|
||||||
const source = `{width:100px}`;
|
const source = `{width:100px}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
<p><img style="--HB_src:url(url); width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
<p><img style="--HB_src:url(url); width:100px;" loading="lazy" src="url" alt="alt text"></p>`.trimReturns());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders normal links', function() {
|
it('Renders normal links', function() {
|
||||||
@@ -438,25 +438,25 @@ describe('Regression Tests', ()=>{
|
|||||||
it('Handle Extra spaces in image alt-text 1', function(){
|
it('Handle Extra spaces in image alt-text 1', function(){
|
||||||
const source='';
|
const source='';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
expect(rendered).toBe('<p><img loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handle Extra spaces in image alt-text 2', function(){
|
it('Handle Extra spaces in image alt-text 2', function(){
|
||||||
const source='';
|
const source='';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
expect(rendered).toBe('<p><img loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handle Extra spaces in image alt-text 3', function(){
|
it('Handle Extra spaces in image alt-text 3', function(){
|
||||||
const source='';
|
const source='';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered).toBe('<p><img src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
expect(rendered).toBe('<p><img loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\"></p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handle Extra spaces in image alt-text 4', function(){
|
it('Handle Extra spaces in image alt-text 4', function(){
|
||||||
const source='{height=20%,width=20%}';
|
const source='{height=20%,width=20%}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered).toBe('<p><img style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" height=\"20%\" width=\"20%\"></p>');
|
expect(rendered).toBe('<p><img style=\"--HB_src:url(http://i.imgur.com/hMna6G0.png);\" loading="lazy" src=\"http://i.imgur.com/hMna6G0.png\" alt=\"where is my image??\" height=\"20%\" width=\"20%\"></p>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ export default {
|
|||||||
const styles = ()=>{
|
const styles = ()=>{
|
||||||
switch (side) {
|
switch (side) {
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
return `{width:100%,bottom:0%}`
|
return `{width:100%,bottom:0%}`;
|
||||||
break;
|
break;
|
||||||
case 'top':
|
case 'top':
|
||||||
return `{width:100%,top:0%}`
|
return `{width:100%,top:0%}`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return `{height:100%}`
|
return `{height:100%}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const rotation = {
|
const rotation = {
|
||||||
'bottom' : 0,
|
'bottom' : 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user