mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-09-22 14:12:58 +00:00
Merge branch 'master' into v4Metadata
This commit is contained in:
@@ -111,6 +111,10 @@ body {
|
|||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
border-right : 1px solid;
|
border-right : 1px solid;
|
||||||
|
max-width:50ch;
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child { border-right : none; }
|
&:last-child { border-right : none; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,176 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
|
import Moment from 'moment';
|
||||||
|
|
||||||
const BrewCleanup = createReactClass({
|
const BrewCleanup = ({})=>{
|
||||||
displayName : 'BrewCleanup',
|
const [junkBrewCollection, setJunkBrewCollection] = useState([]);
|
||||||
getDefaultProps(){
|
const [lostBrewCollection, setLostBrewCollection] = useState([]);
|
||||||
return {};
|
const [pendingJunk, setPendingJunk] = useState(false);
|
||||||
},
|
const [pendingLost, setPendingLost] = useState(false);
|
||||||
getInitialState() {
|
const [error, setError] = useState(null);
|
||||||
return {
|
|
||||||
count : 0,
|
|
||||||
|
|
||||||
pending : false,
|
const find = async (type)=>{
|
||||||
primed : false,
|
|
||||||
err : null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
prime(){
|
|
||||||
this.setState({ pending: true });
|
|
||||||
|
|
||||||
request.get('/admin/cleanup')
|
if(type === 'junk') try {
|
||||||
.then((res)=>this.setState({ count: res.body.count, primed: true }))
|
setPendingJunk(true);
|
||||||
.catch((err)=>this.setState({ error: err }))
|
const res = await request.get('/admin/cleanupJunk');
|
||||||
.finally(()=>this.setState({ pending: false }));
|
|
||||||
},
|
|
||||||
cleanup(){
|
|
||||||
this.setState({ pending: true });
|
|
||||||
|
|
||||||
request.post('/admin/cleanup')
|
setJunkBrewCollection(res.body.brewCollection);
|
||||||
.then((res)=>this.setState({ count: res.body.count }))
|
} catch (err) {
|
||||||
.catch((err)=>this.setState({ error: err }))
|
setError(err);
|
||||||
.finally(()=>this.setState({ pending: false, primed: false }));
|
} finally {
|
||||||
},
|
setPendingJunk(false);
|
||||||
renderPrimed(){
|
|
||||||
if(!this.state.primed) return;
|
|
||||||
|
|
||||||
if(!this.state.count){
|
|
||||||
return <div className='result noBrews'>No Matching Brews found.</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(type === 'lost') try {
|
||||||
|
setPendingLost(true);
|
||||||
|
const res = await request.get('/admin/cleanupLost');
|
||||||
|
|
||||||
|
setLostBrewCollection(res.body.brewCollection);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setPendingLost(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = async (type)=>{
|
||||||
|
|
||||||
|
if(type === 'junk') try {
|
||||||
|
setPendingJunk(true);
|
||||||
|
console.log('deleting junk');
|
||||||
|
const res = await request.post('/admin/cleanupJunk');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setPendingJunk(false);
|
||||||
|
setJunkBrewCollection([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type === 'lost') try {
|
||||||
|
setPendingLost(true);
|
||||||
|
const res = await request.post('/admin/cleanupLost');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setPendingLost(false);
|
||||||
|
setLostBrewCollection([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBrewList = (type)=>{
|
||||||
|
const brewList = type === 'lost' ? lostBrewCollection : junkBrewCollection;
|
||||||
|
|
||||||
|
if(!brewList || brewList.length === 0) {
|
||||||
|
return <>
|
||||||
|
<h3>{`Results - No brews found` }</h3>
|
||||||
|
<table className='resultsTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th>last viewed</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4}><strong>"No brews found"</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
console.log(type);
|
||||||
|
console.log(brewList);
|
||||||
|
return <>
|
||||||
|
<h3>{`Results - ${brewList.length} brews` }</h3>
|
||||||
|
<table className='resultsTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th>last viewed</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{brewList
|
||||||
|
.sort((a, b)=>{ // Sort brews from most recently updated
|
||||||
|
if(a.lastViewed > b.lastViewed) return -1;
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.map((brew, idx)=>{
|
||||||
|
return <tr key={idx}>
|
||||||
|
<td><strong>{brew.title || 'No Title'}</strong></td>
|
||||||
|
<td style={{ width: '200px' }}>{Moment(brew.updatedAt).fromNow()}</td>
|
||||||
|
<td>{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'}</td>
|
||||||
|
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
|
||||||
|
</tr>
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
const renderFound = (type)=>{
|
||||||
|
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
|
||||||
|
|
||||||
return <div className='result'>
|
return <div className='result'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
{deleteButton && <button onClick={()=>cleanup(type)} className='remove'>
|
||||||
{this.state.pending
|
{pendingLost && type === "lost" || pendingJunk && type === "junk"
|
||||||
? <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>
|
}
|
||||||
|
{renderBrewList(type)}
|
||||||
</div>;
|
</div>;
|
||||||
},
|
};
|
||||||
render(){
|
const renderJunkBrewCleanup = ()=>{
|
||||||
return <div className='brewUtil brewCleanup'>
|
return <div className='junk'>
|
||||||
<h2> Brew Cleanup </h2>
|
<h3> Junk brews</h3>
|
||||||
<p>Removes very short brews to tidy up the database</p>
|
<p>Queries unauthored brews that have not been viewed or <br/>updated in 30 days and are shorter than 140 bytes (up to 300)</p>
|
||||||
|
|
||||||
<button onClick={this.prime} className='query'>
|
<button onClick={()=>find('junk')} className='query'>
|
||||||
{this.state.pending
|
{pendingJunk
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: 'Query Brews'
|
: 'Query Brews'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
{this.renderPrimed()}
|
{renderFound('junk')}
|
||||||
|
|
||||||
{this.state.error
|
{error && <div className='error noBrews'>{error.toString()}</div>}
|
||||||
&& <div className='error noBrews'>{this.state.error.toString()}</div>
|
|
||||||
}
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
});
|
const renderLostBrewCleanup = ()=>{
|
||||||
|
return <div className='lost'>
|
||||||
|
<h3> Lost brews</h3>
|
||||||
|
<p>Queries unauthored brews that have not been <br/>updated or viewed for 2 years (up to 500)</p>
|
||||||
|
|
||||||
|
<button onClick={()=>find('lost')} className='query'>
|
||||||
|
{pendingLost
|
||||||
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
|
: 'Query Brews'
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{renderFound('lost')}
|
||||||
|
|
||||||
|
{error && <div className='error noBrews'>{error.toString()}</div>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className='brewUtil brewCleanup'>
|
||||||
|
<h2> Brew Cleanup </h2>
|
||||||
|
{renderJunkBrewCleanup()}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
{renderLostBrewCleanup()}
|
||||||
|
|
||||||
|
</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,20 +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, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language';
|
import {
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -37,69 +43,13 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
|||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
const highlightCompartment = new Compartment();
|
const highlightCompartment = new Compartment();
|
||||||
|
|
||||||
import { generalKeymap, markdownKeymap } from './customKeyMaps.js';
|
import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js';
|
||||||
import foldOnPages from './customFolding.js';
|
import foldOnPages from './extensions/customFolding.js';
|
||||||
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
import { customHighlightPlugin, customHighlightStyle } from './extensions/customHighlight.js';
|
||||||
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js';
|
import { legacyCustomHighlightStyle } from './extensions/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({
|
||||||
@@ -206,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'];
|
||||||
|
|
||||||
@@ -230,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(),
|
||||||
@@ -371,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]);
|
||||||
|
|
||||||
@@ -383,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)
|
||||||
);
|
);
|
||||||
@@ -454,4 +399,4 @@ const CodeEditor = forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default CodeEditor;
|
export default CodeEditor;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -161,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;
|
||||||
|
|||||||
+166
-5
@@ -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 = {
|
||||||
@@ -23,7 +33,7 @@ const customTags = {
|
|||||||
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');
|
||||||
|
|
||||||
@@ -149,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++) {
|
||||||
@@ -158,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +210,7 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
if(lineText.includes('{') && lineText.includes('}')) {
|
if(lineText.includes('{') && lineText.includes('}')) {
|
||||||
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
|
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,
|
||||||
@@ -241,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;
|
||||||
tokens.push({ line: lineNumber, type: customTags.block });
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
||||||
|
|
||||||
@@ -307,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 }
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,8 +18,7 @@ const SplitPane = (props)=>{
|
|||||||
const [liveScroll, setLiveScroll] = useState(false);
|
const [liveScroll, setLiveScroll] = useState(false);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
|
handleResize();
|
||||||
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
|
||||||
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
|
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
@@ -29,7 +28,10 @@ const SplitPane = (props)=>{
|
|||||||
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
||||||
|
|
||||||
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
||||||
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
const handleResize = ()=>{
|
||||||
|
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
|
||||||
|
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUp =(e)=>{
|
const handleUp =(e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -91,6 +91,7 @@ const BrewPage = (props)=>{
|
|||||||
|
|
||||||
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
||||||
let renderedPages = [];
|
let renderedPages = [];
|
||||||
|
let pageTemplates = [];
|
||||||
let rawPages = [];
|
let rawPages = [];
|
||||||
|
|
||||||
const BrewRenderer = (props)=>{
|
const BrewRenderer = (props)=>{
|
||||||
@@ -208,6 +209,20 @@ const BrewRenderer = (props)=>{
|
|||||||
styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
||||||
classes = [classes, injectedTags.classes].join(' ').trim();
|
classes = [classes, injectedTags.classes].join(' ').trim();
|
||||||
attributes = injectedTags.attributes;
|
attributes = injectedTags.attributes;
|
||||||
|
if(global.enablev4) {
|
||||||
|
if (attributes && Object.hasOwn(attributes, 'hbtemplate')) {
|
||||||
|
pageTemplates[index] = attributes['hbtemplate'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(global.enablev4) {
|
||||||
|
// If we don't have a template for this page, look backwards until one is found or the first page.
|
||||||
|
if(!pageTemplates[index]) {
|
||||||
|
for (let i=index;i>=0; i--) {
|
||||||
|
// If one is found, add the template attribute
|
||||||
|
if (pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
||||||
}
|
}
|
||||||
@@ -221,22 +236,31 @@ 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;
|
||||||
|
|
||||||
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
|
if(rawPages.length != renderedPages.length) { // Re-render all pages when page count changes
|
||||||
renderedPages.length = 0;
|
renderedPages.length = 0;
|
||||||
|
pageTemplates.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
||||||
if(rawPages.length > props.currentEditorCursorPageNum -1)
|
if(rawPages.length > props.currentEditorCursorPageNum -1)
|
||||||
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 +300,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,
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ const Homebrew = (props)=>{
|
|||||||
lang : ''
|
lang : ''
|
||||||
},
|
},
|
||||||
userThemes,
|
userThemes,
|
||||||
brews
|
brews,
|
||||||
|
enablev4
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
global.account = account;
|
||||||
|
global.version = version;
|
||||||
|
global.config = config;
|
||||||
|
global.enablev4 = enablev4;
|
||||||
|
|
||||||
const backgroundObject = ()=>{
|
const backgroundObject = ()=>{
|
||||||
if(config?.deployment || (config?.local && config?.development)) {
|
if(config?.deployment || (config?.local && config?.development)) {
|
||||||
const bgText = config?.deployment || 'Local';
|
const bgText = config?.deployment || 'Local';
|
||||||
|
|||||||
@@ -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>;
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
+2
-1
@@ -7,5 +7,6 @@
|
|||||||
"local_environments" : ["docker", "local"],
|
"local_environments" : ["docker", "local"],
|
||||||
"publicUrl" : "https://homebrewery.naturalcrit.com",
|
"publicUrl" : "https://homebrewery.naturalcrit.com",
|
||||||
"hb_images" : null,
|
"hb_images" : null,
|
||||||
"hb_fonts" : null
|
"hb_fonts" : null,
|
||||||
|
"enablev4" : true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
+1461
-1305
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -86,12 +86,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
"@babel/plugin-transform-runtime": "^7.29.7",
|
||||||
"@babel/preset-env": "^7.29.5",
|
"@babel/preset-env": "^7.29.5",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.29.7",
|
||||||
"@babel/runtime": "^7.29.2",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@codemirror/autocomplete": "^6.20.2",
|
"@codemirror/autocomplete": "^6.20.3",
|
||||||
"@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,9 +101,9 @@
|
|||||||
"@codemirror/language-data": "^6.5.2",
|
"@codemirror/language-data": "^6.5.2",
|
||||||
"@codemirror/search": "^6.6.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
"@codemirror/state": "^6.6.0",
|
"@codemirror/state": "^6.6.0",
|
||||||
"@codemirror/view": "^6.43.0",
|
"@codemirror/view": "^6.43.1",
|
||||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||||
"@googleapis/drive": "^20.1.0",
|
"@googleapis/drive": "^20.2.0",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@sanity/diff-match-patch": "^3.2.0",
|
"@sanity/diff-match-patch": "^3.2.0",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
@@ -114,38 +114,38 @@
|
|||||||
"core-js": "^3.49.0",
|
"core-js": "^3.49.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent": "^1.7.1",
|
"dedent": "^1.7.2",
|
||||||
"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.1",
|
"express-static-gzip": "3.0.1",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.3",
|
||||||
"fs-extra": "^11.3.5",
|
"fs-extra": "^11.3.5",
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.5",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.2.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^4.6.4",
|
"less": "^4.6.4",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"marked": "15.0.12",
|
"marked": "15.0.12",
|
||||||
"marked-alignment-paragraphs": "^1.0.0",
|
"marked-alignment-paragraphs": "^1.0.0",
|
||||||
"marked-definition-lists": "^1.0.1",
|
"marked-definition-lists": "^1.0.1",
|
||||||
"marked-emoji": "^2.0.2",
|
"marked-emoji": "^2.0.3",
|
||||||
"marked-extended-tables": "^2.0.1",
|
"marked-extended-tables": "^2.0.1",
|
||||||
"marked-gfm-heading-id": "^4.1.3",
|
"marked-gfm-heading-id": "^4.1.4",
|
||||||
"marked-nonbreaking-spaces": "^1.0.1",
|
"marked-nonbreaking-spaces": "^1.0.1",
|
||||||
"marked-smartypants-lite": "^1.0.3",
|
"marked-smartypants-lite": "^1.0.3",
|
||||||
"marked-subsuper-text": "^1.0.4",
|
"marked-subsuper-text": "^1.0.4",
|
||||||
"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.6.2",
|
"mongoose": "^9.7.0",
|
||||||
"nanoid": "5.1.11",
|
"nanoid": "5.1.11",
|
||||||
"nconf": "^0.13.0",
|
"nconf": "^0.13.0",
|
||||||
"node": "^25.9.0",
|
"node": "^25.9.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.7",
|
||||||
"react-frame-component": "^5.3.2",
|
"react-frame-component": "^5.3.2",
|
||||||
"react-router": "^7.15.1",
|
"react-router": "^7.17.0",
|
||||||
"sanitize-filename": "1.6.4",
|
"sanitize-filename": "1.6.4",
|
||||||
"superagent": "^10.2.1"
|
"superagent": "^10.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
+48
-11
@@ -38,15 +38,30 @@ export default function createAdminApi(vite) {
|
|||||||
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const junkBrewPipeline = [
|
// Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||||
|
const junkBrewsPipeline = [
|
||||||
{ $match : {
|
{ $match : {
|
||||||
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||||
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||||
} },
|
} },
|
||||||
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
|
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1} },
|
||||||
{ $match: { textBinSize: { $lt: 140 } } },
|
{ $match: { textBinSize: { $lt: 140 } } },
|
||||||
{ $limit: 100 }
|
{ $limit: 300 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Search for up to 500 unauthored brews that have not been viewed or updated in two years
|
||||||
|
const lostBrewsPipeline = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
authors: [],
|
||||||
|
updatedAt: { $lt: Moment().subtract(2, 'years').toDate() },
|
||||||
|
lastViewed: { $lt: Moment().subtract(2, 'years').toDate() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$limit: 500
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||||
@@ -54,19 +69,41 @@ export default function createAdminApi(vite) {
|
|||||||
'text' : { '$exists': true }
|
'text' : { '$exists': true }
|
||||||
}).lean().limit(10000).select('_id');
|
}).lean().limit(10000).select('_id');
|
||||||
|
|
||||||
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
|
||||||
.then((objs)=>res.json({ count: objs.length }))
|
|
||||||
.catch((error)=>{
|
.catch((error)=>{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
// Delete result of junkBrewsPipeline
|
||||||
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||||
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
|
.then((docs)=>{
|
||||||
|
const ids = docs.map((doc)=>doc._id);
|
||||||
|
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||||
|
}).then((result)=>{
|
||||||
|
res.json({ count: result.deletedCount });
|
||||||
|
}).catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||||
|
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
|
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
|
||||||
|
.catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete result of lostBrewsPipeline
|
||||||
|
router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||||
|
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
.then((docs)=>{
|
.then((docs)=>{
|
||||||
const ids = docs.map((doc)=>doc._id);
|
const ids = docs.map((doc)=>doc._id);
|
||||||
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||||
|
|||||||
+28
-7
@@ -139,14 +139,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') {
|
||||||
window.frames['BrewRenderer'].contentWindow.print();
|
// fire a custom event for the print cycle
|
||||||
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
|
document.dispatchEvent(new CustomEvent('print:startprep'));
|
||||||
const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0);
|
try {
|
||||||
node.style.display='none';
|
const iframeDoc = window.frames['BrewRenderer'].contentDocument;
|
||||||
node.offsetHeight; // accessing this is enough to trigger a reflow
|
|
||||||
node.style.display='';
|
// 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();
|
||||||
|
|
||||||
|
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
|
||||||
|
const node = iframeDoc.getElementsByClassName('brewRenderer').item(0);
|
||||||
|
node.style.display='none';
|
||||||
|
node.offsetHeight; // accessing this is enough to trigger a reflow
|
||||||
|
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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+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>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user