0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 20:42:43 +00:00

Merge branch 'master' into migration-guide

# Conflicts:
#	shared/naturalcrit/markdown.js
This commit is contained in:
Charlie Humphreys
2021-12-15 23:59:10 -06:00
20 changed files with 1361 additions and 1289 deletions

View File

@@ -3,11 +3,12 @@ h5 {
font-size: .35cm !important;
}
.taskList li {
list-style-type : none;
.page ul ul {
margin-left: 0px;
}
.taskList li input {
list-style-type : none;
margin-left : -0.52cm;
transform: translateY(.05cm);
filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
@@ -33,6 +34,34 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Tuesday 07/12/2021 - v3.0.5
{{taskList
* [x] Fixed paragraph spacing for **note** and **descriptive** boxes in V3.
Fixes issues: [#1836](https://github.com/naturalcrit/homebrewery/issues/1836)
* [x] Added a whole bunch of hotkeys:
* Page Break `CTRL + ENTER`
* Column Break `CTRL + SHIFT + ENTER`
* Bulleted Lists `CTRL + L`
* Numbered Lists `CTRL + SHIFT + L`
* Headers `CTRL + SHIFT + (1-6)`
* Underline `CTRL + U`
* Link `CTRL + K`
* Non-breaking space (\ ) `CTRL + .`
* Add Horizontal Space `CTRL + SHIFT + .`
* Remove Horizontal Space `CTRL + SHIFT + ,`
* Curly Span `CTRL + M`
* Curly Div `CTRL + SHIFT + M`
* [x] Fixed page numbers in the editor panel getting scrambled when scrolling up and down.
* [x] Faster swapping between tabs on long brews.
* [x] Better error messages for common issue with Google Drive credentials expiring.
}}
### Wednesday 17/11/2021 - v3.0.4
{{taskList
* [x] Fixed incorrect sorting of Google brews by page count and views on the user page.

View File

@@ -107,67 +107,69 @@ const Editor = createClass({
if(this.state.view === 'text') {
const codeMirror = this.refs.codeEditor.codeMirror;
//reset custom text styles
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
codeMirror.operation(()=>{ // Batch CodeMirror styling
//reset custom text styles
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
let editorPageCount = 2; // start page count from page 2
//reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background');
codeMirror.removeLineClass(lineNumber, 'text');
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
// Legacy Codemirror styling
if(this.props.renderer == 'legacy') {
if(line.includes('\\page')){
//reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror.removeLineClass(lineNumber, 'text');
// Styling for \page breaks
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
// add back the original class 'background' but also add the new class '.pageline'
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
}
const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count',
textContent : editorPageCount
});
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.match(/^\\page$/)){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
editorPageCount += 1;
};
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
}
}
return r;
}, []);
return lineNumbers;
});
});
}
},

View File

@@ -5,17 +5,13 @@
.codeEditor{
height : 100%;
counter-reset : page;
counter-increment : page;
.pageLine{
background : #33333328;
border-top : #339 solid 1px;
&:after{
content : counter(page);
counter-increment : page;
float : right;
color : gray;
}
}
.editor-page-count{
color : grey;
float : right;
}
.columnSplit{
font-style : italic;

View File

@@ -100,25 +100,25 @@ const subtitles = [
module.exports = ()=>{
return `<style>
.phb#p1{ text-align:center; }
.phb#p1:after{ display:none; }
.phb#p2 { counter-reset:phb-page-numbers; }
.phb:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
.phb:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
.phb:nth-child(2n)::after { transform: scaleX(1); }
.phb:nth-child(2n+1)::after { transform: scaleX(-1); }
.phb:nth-child(2n) .footnote { left: inherit; text-align: right; }
.phb:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
.page#p1{ text-align:center; counter-increment: none; }
.page#p1:after{ display:none; }
.page:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
.page:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
.page:nth-child(2n)::after { transform: scaleX(1); }
.page:nth-child(2n+1)::after { transform: scaleX(-1); }
.page:nth-child(2n) .footnote { left: inherit; text-align: right; }
.page:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
</style>
<div style='margin-top:450px;'></div>
{{margin-top:225px}}
# ${_.sample(titles)}
<div style='margin-top:25px'></div>
<div class='wide'>
{{margin-top:25px}}
{{wide
##### ${_.sample(subtitles)}
</div>
}}
\\page`;
};

View File

@@ -350,14 +350,14 @@ const EditPage = createClass({
</Nav.item>;
}
if(this.state.errors.status == '403' && this.state.errors.response.body.errors[0].reason == 'insufficientPermissions'){
if(this.state.errors.response.req.url.match(/^\/api\/.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
Looks like your Google credentials have
expired! Visit the log in page to sign out
and sign back in with Google
to save this to Google Drive!
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>

View File

@@ -227,14 +227,14 @@ const NewPage = createClass({
</Nav.item>;
}
if(this.state.errors.status == '403' && this.state.errors.response.body.errors[0].reason == 'insufficientPermissions'){
if(this.state.errors.response.req.url.match(/^\/api\/.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
Looks like your Google credentials have
expired! Visit the log in page to sign out
and sign back in with Google
to save this to Google Drive!
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>

View File

@@ -41,7 +41,7 @@ const SharePage = createClass({
if(!(e.ctrlKey || e.metaKey)) return;
const P_KEY = 80;
if(e.keyCode == P_KEY){
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
e.stopPropagation();
e.preventDefault();
}

View File

@@ -48,7 +48,7 @@ const BrewItem = createClass({
renderDeleteBrewLink : function(){
if(!this.props.brew.editId) return;
return <a onClick={this.deleteBrew}>
return <a className='deleteLink' onClick={this.deleteBrew}>
<i className='fas fa-trash-alt' title='Delete' />
</a>;
},
@@ -61,7 +61,7 @@ const BrewItem = createClass({
editLink = this.props.brew.googleId + editLink;
}
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fas fa-pencil-alt' title='Edit' />
</a>;
},
@@ -74,7 +74,7 @@ const BrewItem = createClass({
shareLink = this.props.brew.googleId + shareLink;
}
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fas fa-share-alt' title='Share' />
</a>;
},
@@ -87,7 +87,7 @@ const BrewItem = createClass({
shareLink = this.props.brew.googleId + shareLink;
}
return <a href={`/download/${shareLink}`}>
return <a className='downloadLink' href={`/download/${shareLink}`}>
<i className='fas fa-download' title='Download' />
</a>;
},

View File

@@ -86,7 +86,7 @@ const UserPage = createClass({
<button
value={`${sortValue}`}
onClick={this.handleSortOptionChange}
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
className={`sortOption ${(this.state.sortType == sortValue ? 'active' : '')}`}
>
{`${sortTitle}`}
</button>
@@ -102,7 +102,7 @@ const UserPage = createClass({
renderFilterOption : function(){
return <td>
<label>
<label className='filterOption'>
<i className='fas fa-search'></i>
<input
type='search'

View File

@@ -2,5 +2,6 @@
"host" : "homebrewery.local.naturalcrit.com:8000",
"naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret",
"web_port" : 8000
"web_port" : 8000,
"enable_v3" : true
}

2037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.0.4",
"version": "3.0.5",
"engines": {
"node": "16.11.x"
},
@@ -40,28 +40,30 @@
]
},
"dependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"body-parser": "^1.19.0",
"@babel/core": "^7.16.5",
"@babel/plugin-transform-runtime": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@babel/preset-react": "^7.16.5",
"body-parser": "^1.19.1",
"classnames": "^2.3.1",
"codemirror": "^5.64.0",
"cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.1",
"express": "^4.17.1",
"express-async-handler": "^1.1.4",
"express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.1",
"fs-extra": "10.0.0",
"googleapis": "92.0.0",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "3.0.8",
"marked": "4.0.7",
"marked-extended-tables": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1",
"mongoose": "^6.0.14",
"mongoose": "^6.1.2",
"nanoid": "3.1.30",
"nconf": "^0.11.3",
"prop-types": "15.7.2",
@@ -75,7 +77,7 @@
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.3.0",
"eslint": "^8.4.1",
"eslint-plugin-react": "^7.27.1",
"pico-check": "^2.1.3"
}

View File

@@ -14,6 +14,16 @@
"codemirror/mode/javascript/javascript.js",
"codemirror/addon/fold/foldcode.js",
"codemirror/addon/fold/foldgutter.js",
"codemirror/addon/fold/xml-fold.js",
"codemirror/addon/search/search.js",
"codemirror/addon/search/searchcursor.js",
"codemirror/addon/search/jump-to-line.js",
"codemirror/addon/search/match-highlighter.js",
"codemirror/addon/search/matchesonscrollbar.js",
"codemirror/addon/dialog/dialog.js",
"codemirror/addon/edit/closetag.js",
"codemirror/addon/edit/trailingspace.js",
"codemirror/addon/selection/active-line.js",
"moment",
"superagent",
"marked"

View File

@@ -2,6 +2,7 @@
const _ = require('lodash');
const jwt = require('jwt-simple');
const express = require('express');
const yaml = require('js-yaml');
const app = express();
const homebrewApi = require('./server/homebrew.api.js');
@@ -32,7 +33,7 @@ const getBrewFromId = asyncHandler(async (id, accessType)=>{
if(accessType == 'raw') {
return brew;
}
splitTextAndStyle(brew);
splitTextStyleAndMetadata(brew);
return brew;
});
@@ -45,8 +46,15 @@ const sanitizeBrew = (brew, full=false)=>{
return brew;
};
const splitTextAndStyle = (brew)=>{
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) {
const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
brew.text = brew.text.slice(index + 5);
}
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
@@ -129,7 +137,7 @@ app.get('/v3_preview', async (req, res, next)=>{
text : welcomeTextV3,
renderer : 'V3'
};
splitTextAndStyle(brew);
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
@@ -152,7 +160,7 @@ app.get('/changelog', async (req, res, next)=>{
text : changelogText,
renderer : 'V3'
};
splitTextAndStyle(brew);
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
@@ -164,7 +172,7 @@ app.get('/faq', async (req, res, next)=>{
text : faqText,
renderer : 'V3'
};
splitTextAndStyle(brew);
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});

View File

@@ -4,6 +4,7 @@ const router = require('express').Router();
const zlib = require('zlib');
const GoogleActions = require('./googleActions.js');
const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml');
// const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
@@ -11,6 +12,22 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
// });
// };
const mergeBrewText = (brew)=>{
let text = brew.text;
if(brew.style !== undefined) {
text = `\`\`\`css\n` +
`${brew.style || ''}\n` +
`\`\`\`\n\n` +
`${text}`;
}
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer']);
text = `\`\`\`metadata\n` +
`${yaml.dump(metadata)}\n` +
`\`\`\`\n\n` +
`${text}`;
return text;
};
const MAX_TITLE_LENGTH = 100;
const getGoodBrewTitle = (text)=>{
@@ -28,16 +45,6 @@ const excludePropsFromUpdate = (brew)=>{
return brew;
};
const mergeBrewText = (text, style)=>{
if(typeof style !== 'undefined') {
text = `\`\`\`css\n` +
`${style}\n` +
`\`\`\`\n\n` +
`${text}`;
}
return text;
};
const newBrew = (req, res)=>{
const brew = req.body;
@@ -46,7 +53,7 @@ const newBrew = (req, res)=>{
}
brew.authors = (req.account) ? [req.account.username] : [];
brew.text = mergeBrewText(brew.text, brew.style);
brew.text = mergeBrewText(brew);
delete brew.editId;
delete brew.shareId;
@@ -75,7 +82,7 @@ const updateBrew = (req, res)=>{
.then((brew)=>{
const updateBrew = excludePropsFromUpdate(req.body);
brew = _.merge(brew, updateBrew);
brew.text = mergeBrewText(brew.text, brew.style);
brew.text = mergeBrewText(brew);
// Compress brew text to binary before saving
brew.textBin = zlib.deflateRawSync(brew.text);
@@ -143,7 +150,7 @@ const newGoogleBrew = async (req, res, next)=>{
}
brew.authors = (req.account) ? [req.account.username] : [];
brew.text = mergeBrewText(brew.text, brew.style);
brew.text = mergeBrewText(brew);
delete brew.editId;
delete brew.shareId;
@@ -165,13 +172,13 @@ const updateGoogleBrew = async (req, res, next)=>{
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
const brew = excludePropsFromUpdate(req.body);
brew.text = mergeBrewText(brew.text, brew.style);
brew.text = mergeBrewText(brew);
try {
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
return res.status(200).send(updatedBrew);
} catch (err) {
return res.status(err.response.status).send(err);
return res.status(err.response?.status || 500).send(err);
}
};

View File

@@ -0,0 +1,48 @@
const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
const ranges = cm.listSelections(), replacements = [];
for (let i = 0; i < ranges.length; i++) {
if(!ranges[i].empty()) return CodeMirror.Pass;
const pos = ranges[i].head, line = cm.getLine(pos.line), tok = cm.getTokenAt(pos);
if(!typingClosingBrace && (tok.type == 'string' || tok.string.charAt(0) != '{' || tok.start != pos.ch - 1))
return CodeMirror.Pass;
else if(typingClosingBrace) {
let hasUnclosedBraces = false, index = -1;
do {
index = line.indexOf('{{', index + 1);
if(index !== -1 && line.indexOf('}}', index + 1) === -1) {
hasUnclosedBraces = true;
break;
}
} while (index !== -1);
if(!hasUnclosedBraces) return CodeMirror.Pass;
}
replacements[i] = typingClosingBrace ? {
text : '}}',
newPos : CodeMirror.Pos(pos.line, pos.ch + 2)
} : {
text : '{}}',
newPos : CodeMirror.Pos(pos.line, pos.ch + 1)
};
}
for (let i = ranges.length - 1; i >= 0; i--) {
const info = replacements[i];
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, '+insert');
const sel = cm.listSelections().slice(0);
sel[i] = {
head : info.newPos,
anchor : info.newPos
};
cm.setSelections(sel);
}
};
module.exports = {
autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
const map = { name: 'autoCloseCurlyBraces' };
map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
codeMirror.addKeyMap(map);
}
};

View File

@@ -4,7 +4,7 @@ const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const closeTag = require('./close-tag');
let CodeMirror;
if(typeof navigator !== 'undefined'){
@@ -16,8 +16,24 @@ if(typeof navigator !== 'undefined'){
require('codemirror/mode/javascript/javascript.js');
//Addons
//Code folding
require('codemirror/addon/fold/foldcode.js');
require('codemirror/addon/fold/foldgutter.js');
//Search and replace
require('codemirror/addon/search/search.js');
require('codemirror/addon/search/searchcursor.js');
require('codemirror/addon/search/jump-to-line.js');
require('codemirror/addon/search/match-highlighter.js');
require('codemirror/addon/search/matchesonscrollbar.js');
require('codemirror/addon/dialog/dialog.js');
//Trailing space highlighting
require('codemirror/addon/edit/trailingspace.js');
//Active line highlighting
require('codemirror/addon/selection/active-line.js');
//Auto-closing
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
require('codemirror/addon/fold/xml-fold.js');
require('codemirror/addon/edit/closetag.js');
const foldCode = require('./fold-code');
foldCode.registerHomebreweryHelper(CodeMirror);
@@ -115,6 +131,9 @@ const CodeEditor = createClass({
'Shift-Cmd-Enter' : this.newColumn,
'Ctrl-Enter' : this.newPage,
'Cmd-Enter' : this.newPage,
'Ctrl-F' : 'findPersistent',
'Cmd-F' : 'findPersistent',
'Shift-Enter' : 'findPersistentPrevious',
'Ctrl-[' : this.foldAllCode,
'Cmd-[' : this.foldAllCode,
'Ctrl-]' : this.unfoldAllCode,
@@ -142,8 +161,19 @@ const CodeEditor = createClass({
return `\u21A4 ${text} \u21A6`;
}
},
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseTags : true,
styleActiveLine : true,
showTrailingSpace : true,
specialChars : / /,
specialCharPlaceholder : function(char) {
const el = document.createElement('span');
el.className = 'cm-space';
el.innerHTML = ' ';
return el;
}
});
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});

View File

@@ -1,5 +1,7 @@
@import (less) 'codemirror/lib/codemirror.css';
@import (less) 'codemirror/addon/fold/foldgutter.css';
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
@import (less) 'codemirror/addon/dialog/dialog.css';
.codeEditor{
.CodeMirror-foldmarker {
@@ -7,4 +9,14 @@
text-shadow: none;
font-weight: 600;
}
.cm-tab {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
}
.cm-trailingspace {
.cm-space {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
}
}
}

View File

@@ -1,7 +1,8 @@
/* eslint-disable max-lines */
const _ = require('lodash');
const Markdown = require('marked');
const renderer = new Markdown.Renderer();
const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables');
const renderer = new Marked.Renderer();
//Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) {
@@ -9,7 +10,7 @@ renderer.html = function (html) {
const openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1);
html = html.substring(0, html.lastIndexOf('</div>'));
return `${openTag} ${Markdown(html)} </div>`;
return `${openTag} ${Marked.parse(html)} </div>`;
}
return html;
};
@@ -235,200 +236,10 @@ const definitionLists = {
}
};
const spanTable = {
name : 'spanTable',
level : 'block', // Is this a block-level or inline-level tokenizer?
start(src) { return src.match(/^\n *([^\n ].*\|.*)\n/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
//const regex = this.tokenizer.rules.block.table;
const regex = new RegExp('^ *([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' // Header
+ ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)\\|?' // Align
+ '(?:\\n *((?:(?!\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells
+ '(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}'
+ '(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |'
+ '<\\/?(?:address|article|aside|base|basefont|blockquote|body|'
+ 'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)'); // Cells
const cap = regex.exec(src);
if(cap) {
const item = {
type : 'spanTable',
header : cap[1].replace(/\n$/, '').split('\n'),
align : cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
rows : cap[3] ? cap[3].replace(/\n$/, '').split('\n') : []
};
// Get first header row to determine how many columns
item.header[0] = splitCells(item.header[0]);
const colCount = item.header[0].reduce((length, header)=>{
return length + header.colspan;
}, 0);
if(colCount === item.align.length) {
item.raw = cap[0];
let i, j, k, row;
// Get alignment row (:---:)
let l = item.align.length;
for (i = 0; i < l; i++) {
if(/^ *-+: *$/.test(item.align[i])) {
item.align[i] = 'right';
} else if(/^ *:-+: *$/.test(item.align[i])) {
item.align[i] = 'center';
} else if(/^ *:-+ *$/.test(item.align[i])) {
item.align[i] = 'left';
} else {
item.align[i] = null;
}
}
// Get any remaining header rows
l = item.header.length;
for (i = 1; i < l; i++) {
item.header[i] = splitCells(item.header[i], colCount, item.header[i-1]);
}
// Get main table cells
l = item.rows.length;
for (i = 0; i < l; i++) {
item.rows[i] = splitCells(item.rows[i], colCount, item.rows[i-1]);
}
// header child tokens
l = item.header.length;
for (j = 0; j < l; j++) {
row = item.header[j];
for (k = 0; k < row.length; k++) {
row[k].tokens = [];
this.lexer.inlineTokens(row[k].text, row[k].tokens);
}
}
// cell child tokens
l = item.rows.length;
for (j = 0; j < l; j++) {
row = item.rows[j];
for (k = 0; k < row.length; k++) {
row[k].tokens = [];
this.lexer.inlineTokens(row[k].text, row[k].tokens);
}
}
return item;
}
}
},
renderer(token) {
let i, j, row, cell, col, text;
let output = `<table>`;
output += `<thead>`;
for (i = 0; i < token.header.length; i++) {
row = token.header[i];
let col = 0;
output += `<tr>`;
for (j = 0; j < row.length; j++) {
cell = row[j];
text = this.parser.parseInline(cell.tokens);
output += getTableCell(text, cell, 'th', token.align[col]);
col += cell.colspan;
}
output += `</tr>`;
}
output += `</thead>`;
if(token.rows.length) {
output += `<tbody>`;
for (i = 0; i < token.rows.length; i++) {
row = token.rows[i];
col = 0;
output += `<tr>`;
for (j = 0; j < row.length; j++) {
cell = row[j];
text = this.parser.parseInline(cell.tokens);
output += getTableCell(text, cell, 'td', token.align[col]);
col += cell.colspan;
}
output += `</tr>`;
}
output += `</tbody>`;
}
output += `</table>`;
return output;
}
};
const getTableCell = (text, cell, type, align)=>{
if(!cell.rowspan) {
return '';
}
const tag = `<${type}`
+ `${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}`
+ `${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}`
+ `${align ? ` align=${align}` : ''}>`;
return `${tag + text}</${type}>\n`;
};
const splitCells = (tableRow, count, prevRow = [])=>{
const cells = [...tableRow.matchAll(/(?:[^|\\]|\\.?)+(?:\|+|$)/g)].map((x)=>x[0]);
// Remove first/last cell in a row if whitespace only and no leading/trailing pipe
if(!cells[0]?.trim()) { cells.shift(); }
if(!cells[cells.length - 1]?.trim()) { cells.pop(); }
let numCols = 0;
let i, j, trimmedCell, prevCell, prevCols;
for (i = 0; i < cells.length; i++) {
trimmedCell = cells[i].split(/\|+$/)[0];
cells[i] = {
rowspan : 1,
colspan : Math.max(cells[i].length - trimmedCell.length, 1),
text : trimmedCell.trim().replace(/\\\|/g, '|')
// display escaped pipes as normal character
};
// Handle Rowspan
if(trimmedCell.slice(-1) == '^' && prevRow.length) {
// Find matching cell in previous row
prevCols = 0;
for (j = 0; j < prevRow.length; j++) {
prevCell = prevRow[j];
if((prevCols == numCols) && (prevCell.colspan == cells[i].colspan)) {
// merge into matching cell in previous row (the "target")
cells[i].rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
cells[i].rowSpanTarget.text += ` ${cells[i].text.slice(0, -1)}`;
cells[i].rowSpanTarget.rowspan += 1;
cells[i].rowspan = 0;
break;
}
prevCols += prevCell.colspan;
if(prevCols > numCols)
break;
}
}
numCols += cells[i].colspan;
}
// Force main cell rows to match header column count
if(numCols > count) {
cells.splice(count);
} else {
while (numCols < count) {
cells.push({
colspan : 1,
text : ''
});
numCols += 1;
}
}
return cells;
};
Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, spanTable] });
Markdown.use(mustacheInjectBlock);
Markdown.use({ smartypants: true });
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
Marked.use(MarkedExtendedTables());
Marked.use(mustacheInjectBlock);
Marked.use({ smartypants: true });
//Fix local links in the Preview iFrame to link inside the frame
renderer.link = function (href, title, text) {
@@ -532,11 +343,11 @@ const processStyleTags = (string)=>{
};
module.exports = {
marked : Markdown,
marked : Marked,
render : (rawBrewText)=>{
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Markdown(
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Marked.parse(
sanatizeScriptTags(rawBrewText),
{ renderer: renderer }
);

View File

@@ -511,7 +511,8 @@ body {
color : #58180d;
background-color : #faf7ea;
border-radius : 4px;
white-space : pre-wrap
white-space : pre-wrap;
overflow-wrap : break-word;
}
pre code{
@@ -652,7 +653,7 @@ body {
break-inside : avoid;
h1 {
text-align : center;
margin-bottom : 0cm;
margin-bottom : 0.3cm;
}
a{
display : table;
@@ -663,14 +664,12 @@ body {
}
}
h4 {
margin-top : 0.14cm;
margin-top : 0.2cm;
line-height : 0.4cm;
& + ul li {
line-height: 1.2em;
}
}
& > ul {
margin-top: 0.52cm;
}
ul{
padding-left : 0;
list-style-type : none;