mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 18:32:41 +00:00
Merge branch 'master' into Issue_1430_Unique_HeaderIDs
This commit is contained in:
30
changelog.md
30
changelog.md
@@ -84,6 +84,34 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Monday 7/29/2024 - v3.14.0
|
||||
{{taskList
|
||||
|
||||
##### abquintic, calculuschild
|
||||
|
||||
* [x] Alternative Brew Themes, including importing other brews as a base theme.
|
||||
|
||||
- In the :fas_circle_info: **Properties** menu, find the new {{openSans **THEME**}} dropdown. It lists Brew Themes, including a new **Blank** theme as a simpler basis for custom styling.
|
||||
- Brews tagged with `meta:theme` will appear in the Brew Themes list. Selecting one loads its :fas_paintbrush: **Style** tab contents as the CSS basis for the current brew, allowing one brew to style multiple documents.
|
||||
- Brews with `meta:theme` can also select their own Theme, i.e. layering Themes on top of each other.
|
||||
- The next goal is to make **Published** Themes shareable between users.
|
||||
|
||||
|
||||
Fixes issues [#1899](https://github.com/naturalcrit/homebrewery/issues/1899), [#3085](https://github.com/naturalcrit/homebrewery/issues/3085)
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix Drop-cap font becoming corrupted when Bold
|
||||
|
||||
Fixes issues [#3551](https://github.com/naturalcrit/homebrewery/issues/3551)
|
||||
|
||||
* [x] Fixes to UI styling
|
||||
|
||||
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
||||
|
||||
}}
|
||||
|
||||
|
||||
### Saturday 6/7/2024 - v3.13.1
|
||||
{{taskList
|
||||
|
||||
@@ -131,8 +159,6 @@ Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
|
||||
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
### Monday 18/3/2024 - v3.12.0
|
||||
{{taskList
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||
const DOMPurify = require('dompurify');
|
||||
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
|
||||
const INITIAL_CONTENT = dedent`
|
||||
@@ -57,6 +55,7 @@ const BrewRenderer = (props)=>{
|
||||
lang : '',
|
||||
errors : [],
|
||||
currentEditorPage : 0,
|
||||
themeBundle : {},
|
||||
...props
|
||||
};
|
||||
|
||||
@@ -125,10 +124,9 @@ const BrewRenderer = (props)=>{
|
||||
};
|
||||
|
||||
const renderStyle = ()=>{
|
||||
if(!props.style) return;
|
||||
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />;
|
||||
};
|
||||
|
||||
const renderPage = (pageText, index)=>{
|
||||
@@ -188,10 +186,6 @@ const BrewRenderer = (props)=>{
|
||||
document.dispatchEvent(new MouseEvent('click'));
|
||||
};
|
||||
|
||||
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = props.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*render dummy page while iFrame is mounting.*/}
|
||||
@@ -220,13 +214,6 @@ const BrewRenderer = (props)=>{
|
||||
onKeyDown={handleControlKeys}
|
||||
tabIndex={-1}
|
||||
style={{ height: state.height }}>
|
||||
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type='text/css' rel='stylesheet'/>
|
||||
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{state.isMounted
|
||||
&&
|
||||
|
||||
@@ -381,7 +381,8 @@ const Editor = createClass({
|
||||
<MetadataEditor
|
||||
metadata={this.props.brew}
|
||||
onChange={this.props.onMetaChange}
|
||||
reportError={this.props.reportError}/>
|
||||
reportError={this.props.reportError}
|
||||
userThemes={this.props.userThemes}/>
|
||||
</>;
|
||||
}
|
||||
},
|
||||
@@ -424,6 +425,7 @@ const Editor = createClass({
|
||||
historySize={this.historySize()}
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
snippetBundle={this.props.snippetBundle}
|
||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
||||
|
||||
{this.renderEditor()}
|
||||
|
||||
@@ -8,6 +8,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Combobox = require('client/components/combobox.jsx');
|
||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
const validations = require('./validations.js');
|
||||
|
||||
@@ -98,7 +99,7 @@ const MetadataEditor = createClass({
|
||||
if(renderer == 'legacy')
|
||||
this.props.metadata.theme = '5ePHB';
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
this.props.onChange(this.props.metadata, 'renderer');
|
||||
},
|
||||
handlePublish : function(val){
|
||||
this.props.onChange({
|
||||
@@ -110,7 +111,7 @@ const MetadataEditor = createClass({
|
||||
handleTheme : function(theme){
|
||||
this.props.metadata.renderer = theme.renderer;
|
||||
this.props.metadata.theme = theme.path;
|
||||
this.props.onChange(this.props.metadata);
|
||||
this.props.onChange(this.props.metadata, 'theme');
|
||||
},
|
||||
|
||||
handleLanguage : function(languageCode){
|
||||
@@ -191,37 +192,41 @@ const MetadataEditor = createClass({
|
||||
renderThemeDropdown : function(){
|
||||
if(!global.enable_themes) return;
|
||||
|
||||
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
||||
|
||||
const listThemes = (renderer)=>{
|
||||
return _.map(_.values(Themes[renderer]), (theme)=>{
|
||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||
{`${theme.renderer} : ${theme.name}`}
|
||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
||||
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||
{theme.author ?? renderer} : {theme.name}
|
||||
<div className='texture-container'>
|
||||
<img src={texture}/>
|
||||
</div>
|
||||
<div className='preview'>
|
||||
<h6>{`${theme.name}`} preview</h6>
|
||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
|
||||
<h6>{theme.name} preview</h6>
|
||||
<img src={preview}/>
|
||||
</div>
|
||||
</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
||||
const currentRenderer = this.props.metadata.renderer;
|
||||
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
||||
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
||||
let dropdown;
|
||||
|
||||
if(this.props.metadata.renderer == 'legacy') {
|
||||
if(currentRenderer == 'legacy') {
|
||||
dropdown =
|
||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||
<div>
|
||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
||||
</Nav.dropdown>;
|
||||
} else {
|
||||
dropdown =
|
||||
<Nav.dropdown className='value' trigger='click'>
|
||||
<div>
|
||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
{/*listThemes('Legacy')*/}
|
||||
{listThemes('V3')}
|
||||
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
||||
|
||||
{listThemes(currentRenderer)}
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,13 @@
|
||||
color : white;
|
||||
}
|
||||
}
|
||||
.navDropdown .item > p {
|
||||
width: 45%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
height: 1.1em;
|
||||
}
|
||||
.navDropdown {
|
||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
position : absolute;
|
||||
@@ -230,14 +237,23 @@
|
||||
&:hover > .preview {
|
||||
opacity: 1;
|
||||
}
|
||||
>img {
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
position : absolute;
|
||||
right : 0;
|
||||
top : 0px;
|
||||
width : 50%;
|
||||
height : 100%;
|
||||
.texture-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
> img {
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
position : absolute;
|
||||
right : 0;
|
||||
top : 0px;
|
||||
width : 50%;
|
||||
min-height : 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
//Import all themes
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const ThemeSnippets = {};
|
||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||
@@ -40,7 +37,8 @@ const Snippetbar = createClass({
|
||||
foldCode : ()=>{},
|
||||
unfoldCode : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {}
|
||||
cursorPos : {},
|
||||
snippetBundle : []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -53,21 +51,15 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
componentDidMount : async function() {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
const snippets = this.compileSnippets();
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
},
|
||||
|
||||
componentDidUpdate : async function(prevProps) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||
const snippets = this.compileSnippets();
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
@@ -75,26 +67,26 @@ const Snippetbar = createClass({
|
||||
},
|
||||
|
||||
|
||||
mergeCustomizer : function(valueA, valueB, key) {
|
||||
mergeCustomizer : function(oldValue, newValue, key) {
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
||||
}
|
||||
},
|
||||
|
||||
compileSnippets : function(rendererPath, themePath, snippets) {
|
||||
let compiledSnippets = snippets;
|
||||
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
||||
compileSnippets : function() {
|
||||
let compiledSnippets = [];
|
||||
|
||||
const objB = _.keyBy(compiledSnippets, 'groupName');
|
||||
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
|
||||
if(baseSnippetsPath) {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
||||
} else {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
for (let snippets of this.props.snippetBundle) {
|
||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||
snippets = ThemeSnippets[snippets];
|
||||
|
||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||
|
||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||
}
|
||||
return compiledSnippets;
|
||||
},
|
||||
|
||||
@@ -66,10 +66,10 @@ const Homebrew = createClass({
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
|
||||
@@ -104,6 +104,18 @@ const ErrorNavItem = createClass({
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '09') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like there was a problem retreiving
|
||||
the theme, or a theme that it inherits,
|
||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> still exists!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
.lowercase {
|
||||
text-transform : none;
|
||||
}
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
|
||||
@@ -119,11 +119,12 @@
|
||||
text-align : center;
|
||||
a{
|
||||
.animate(opacity);
|
||||
display : block;
|
||||
margin : 8px 0px;
|
||||
opacity : 0.6;
|
||||
font-size : 1.3em;
|
||||
color : white;
|
||||
display : block;
|
||||
margin : 8px 0px;
|
||||
opacity : 0.6;
|
||||
font-size : 1.3em;
|
||||
color : white;
|
||||
text-decoration : unset;
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const googleDriveIcon = require('../../googleDrive.svg');
|
||||
|
||||
@@ -55,7 +55,8 @@ const EditPage = createClass({
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date(),
|
||||
currentEditorPage : 0,
|
||||
displayLockMessage : this.props.brew.lock || false
|
||||
displayLockMessage : this.props.brew.lock || false,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -87,6 +88,8 @@ const EditPage = createClass({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
@@ -130,7 +133,10 @@ const EditPage = createClass({
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
@@ -138,7 +144,6 @@ const EditPage = createClass({
|
||||
},
|
||||
isPending : true,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
@@ -406,12 +411,15 @@ const EditPage = createClass({
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
currentEditorPage={this.state.currentEditorPage}
|
||||
|
||||
@@ -136,6 +136,19 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Theme load error
|
||||
'09' : dedent`
|
||||
## No Homebrewery theme document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. It was likely deleted by
|
||||
its owner.
|
||||
|
||||
:
|
||||
|
||||
**Requested access:** ${props.brew.accessType}
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Brew locked by Administrators error
|
||||
'100' : dedent`
|
||||
## This brew has been locked.
|
||||
|
||||
@@ -13,6 +13,7 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
@@ -34,12 +35,17 @@ const HomePage = createClass({
|
||||
brew : this.props.brew,
|
||||
welcomeText : this.props.brew.text,
|
||||
error : undefined,
|
||||
currentEditorPage : 0
|
||||
currentEditorPage : 0,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
|
||||
handleSave : function(){
|
||||
request.post('/api')
|
||||
.send(this.state.brew)
|
||||
@@ -95,6 +101,7 @@ const HomePage = createClass({
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
currentEditorPage={this.state.currentEditorPage}
|
||||
themeBundle={this.state.themeBundle}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
@@ -44,7 +44,8 @@ const NewPage = createClass({
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(brew.text),
|
||||
currentEditorPage : 0
|
||||
currentEditorPage : 0,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -77,6 +78,8 @@ const NewPage = createClass({
|
||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||
});
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
@@ -122,7 +125,10 @@ const NewPage = createClass({
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, ...metadata },
|
||||
}), ()=>{
|
||||
@@ -142,8 +148,6 @@ const NewPage = createClass({
|
||||
isSaving : true
|
||||
});
|
||||
|
||||
console.log('saving new brew');
|
||||
|
||||
let brew = this.state.brew;
|
||||
// Split out CSS to Style if CSS codefence exists
|
||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||
@@ -153,12 +157,10 @@ const NewPage = createClass({
|
||||
}
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
const res = await request
|
||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
this.setState({ isSaving: false, error: err });
|
||||
});
|
||||
if(!res) return;
|
||||
@@ -214,12 +216,14 @@ const NewPage = createClass({
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
currentEditorPage={this.state.currentEditorPage}
|
||||
|
||||
@@ -12,18 +12,26 @@ const Account = require('../../navbar/account.navitem.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
const SharePage = createClass({
|
||||
displayName : 'SharePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW_LOAD
|
||||
brew : DEFAULT_BREW_LOAD,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
@@ -99,6 +107,7 @@ const SharePage = createClass({
|
||||
style={this.props.brew.style}
|
||||
renderer={this.props.brew.renderer}
|
||||
theme={this.props.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"secret" : "secret",
|
||||
"web_port" : 8000,
|
||||
"enable_v3" : true,
|
||||
"enable_themes" : true,
|
||||
"local_environments" : ["docker", "local"],
|
||||
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
||||
}
|
||||
|
||||
17
package.json
17
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.13.1",
|
||||
"version": "3.14.0",
|
||||
"engines": {
|
||||
"npm": "^10.2.x",
|
||||
"node": "^20.8.x"
|
||||
@@ -22,7 +22,8 @@
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
"verify": "npm run lint && npm test",
|
||||
"test": "jest --runInBand",
|
||||
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||
"test:coverage": "jest --coverage --silent --runInBand",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
@@ -56,15 +57,15 @@
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 25,
|
||||
"branches": 10,
|
||||
"functions": 22,
|
||||
"lines": 25
|
||||
"statements": 50,
|
||||
"branches": 40,
|
||||
"functions": 40,
|
||||
"lines": 50
|
||||
},
|
||||
"server/homebrew.api.js": {
|
||||
"statements": 65,
|
||||
"statements": 70,
|
||||
"branches": 50,
|
||||
"functions": 60,
|
||||
"functions": 65,
|
||||
"lines": 70
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ const yaml = require('js-yaml');
|
||||
const app = express();
|
||||
const config = require('./config.js');
|
||||
|
||||
const { homebrewApi, getBrew } = require('./homebrew.api.js');
|
||||
const { homebrewApi, getBrew, getUsersBrewThemes } = require('./homebrew.api.js');
|
||||
const GoogleActions = require('./googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
@@ -81,7 +81,8 @@ app.get('/robots.txt', (req, res)=>{
|
||||
app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3'
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -97,7 +98,8 @@ app.get('/', (req, res, next)=>{
|
||||
app.get('/legacy', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeTextLegacy,
|
||||
renderer : 'legacy'
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
@@ -265,9 +267,11 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
@@ -279,10 +283,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
return next();
|
||||
});
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
//New Page from ID
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
const brew = {
|
||||
@@ -292,17 +296,31 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
style : req.brew.style,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags
|
||||
tags : req.brew.tags,
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
@@ -418,7 +436,8 @@ const renderPage = async (req, res)=>{
|
||||
enable_v3 : config.get('enable_v3'),
|
||||
enable_themes : config.get('enable_themes'),
|
||||
config : configuration,
|
||||
ogMeta : req.ogMeta
|
||||
ogMeta : req.ogMeta,
|
||||
userThemes : req.userThemes
|
||||
};
|
||||
const title = req.brew ? req.brew.title : '';
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
|
||||
@@ -8,9 +8,16 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||
const yaml = require('js-yaml');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
const { nanoid } = require('nanoid');
|
||||
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||
|
||||
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||
|
||||
const Themes = require('../themes/themes.json');
|
||||
|
||||
const isStaticTheme = (renderer, themeName)=>{
|
||||
return Themes[renderer]?.[themeName] !== undefined;
|
||||
};
|
||||
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
@@ -37,6 +44,40 @@ const api = {
|
||||
}
|
||||
return { id, googleId };
|
||||
},
|
||||
//Get array of any of this user's brews tagged with `meta:theme`
|
||||
getUsersBrewThemes : async (username)=>{
|
||||
const fields = [
|
||||
'title',
|
||||
'tags',
|
||||
'shareId',
|
||||
'thumbnail',
|
||||
'textBin',
|
||||
'text',
|
||||
'authors',
|
||||
'renderer'
|
||||
];
|
||||
|
||||
const userThemes = {};
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||
|
||||
if(brews) {
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return userThemes;
|
||||
},
|
||||
getBrew : (accessType, stubOnly = false)=>{
|
||||
// Create middleware with the accessType passed in as part of the scope
|
||||
return async (req, res, next)=>{
|
||||
@@ -209,6 +250,62 @@ const api = {
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
getThemeBundle : async(req, res)=>{
|
||||
/*
|
||||
getThemeBundle: Collects the theme and all parent themes
|
||||
returns an object containing an array of css, in render order, and an array
|
||||
of snippets ( currently empty )
|
||||
Important parameter members:
|
||||
req.params.id: This is the shareId ( User theme ) or name ( static theme )
|
||||
loaded first.
|
||||
req.params.renderer: This is the Markdown+ version for the static theme. If a
|
||||
User theme the value will come from the User Theme metadata.
|
||||
*/
|
||||
req.params.renderer = _.upperFirst(req.params.renderer);
|
||||
let currentTheme;
|
||||
const completeStyles = [];
|
||||
const completeSnippets = [];
|
||||
|
||||
while (req.params.id) {
|
||||
//=== User Themes ===//
|
||||
if(!isStaticTheme(req.params.renderer, req.params.id)) {
|
||||
await api.getBrew('share')(req, res, ()=>{})
|
||||
.catch((err)=>{
|
||||
if(err.HBErrorCode == '05')
|
||||
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
|
||||
throw err;
|
||||
});
|
||||
|
||||
currentTheme = req.brew;
|
||||
splitTextStyleAndMetadata(currentTheme);
|
||||
|
||||
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
|
||||
|
||||
req.params.id = currentTheme.theme;
|
||||
req.params.renderer = currentTheme.renderer;
|
||||
}
|
||||
//=== Static Themes ===//
|
||||
else {
|
||||
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
||||
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
||||
completeSnippets.push(localSnippets);
|
||||
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
|
||||
|
||||
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
|
||||
}
|
||||
}
|
||||
|
||||
const returnObj = {
|
||||
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
||||
styles : completeStyles.reverse(),
|
||||
snippets : completeSnippets.reverse()
|
||||
};
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(200).send(returnObj);
|
||||
},
|
||||
updateBrew : async (req, res)=>{
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
@@ -369,5 +466,6 @@ router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api
|
||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -14,6 +14,9 @@ describe('Tests for api', ()=>{
|
||||
let saved;
|
||||
|
||||
beforeEach(()=>{
|
||||
jest.resetModules();
|
||||
jest.restoreAllMocks();
|
||||
|
||||
saved = undefined;
|
||||
saveFunc = jest.fn(async function() {
|
||||
saved = { ...this, _id: '1' };
|
||||
@@ -45,8 +48,9 @@ describe('Tests for api', ()=>{
|
||||
model.mockImplementation((brew)=>modelBrew(brew));
|
||||
|
||||
res = {
|
||||
status : jest.fn(()=>res),
|
||||
send : jest.fn(()=>{})
|
||||
status : jest.fn(()=>res),
|
||||
send : jest.fn(()=>{}),
|
||||
setHeader : jest.fn(()=>{})
|
||||
};
|
||||
|
||||
api = require('./homebrew.api');
|
||||
@@ -81,10 +85,6 @@ describe('Tests for api', ()=>{
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getId', ()=>{
|
||||
it('should return only id if google id is not present', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
@@ -581,6 +581,121 @@ brew`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme bundle', ()=>{
|
||||
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
||||
snippets : []
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Theme Bundle for nested User Themes', async ()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : [
|
||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||
],
|
||||
snippets : []
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Theme Bundle for a Static Theme', async ()=>{
|
||||
const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : [
|
||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
||||
],
|
||||
snippets : [
|
||||
'V3_Blank',
|
||||
'V3_5ePHB'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
await api.getThemeBundle(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
styles : [
|
||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
||||
`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
|
||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||
],
|
||||
snippets : [
|
||||
'V3_Blank',
|
||||
'V3_5ePHB',
|
||||
'V3_5eDMG'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for an invalid Theme in the chain', async()=>{
|
||||
const brews = {
|
||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||
};
|
||||
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||
|
||||
let err;
|
||||
await api.getThemeBundle(req, res)
|
||||
.catch((e)=>err = e);
|
||||
|
||||
expect(err).toEqual({
|
||||
HBErrorCode : '09',
|
||||
accessType : 'share',
|
||||
brewId : 'missingTheme',
|
||||
message : 'Theme Not Found',
|
||||
name : 'ThemeLoad Error',
|
||||
status : 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBrew', ()=>{
|
||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||
|
||||
@@ -50,8 +50,8 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
||||
return brew;
|
||||
};
|
||||
|
||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
||||
const query = { authors: username, published: true };
|
||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null, filter=null){
|
||||
const query = { authors: username, published: true, ...filter };
|
||||
if(allowAccess){
|
||||
delete query.published;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const yaml = require('js-yaml');
|
||||
const request = require('../client/homebrew/utils/request-middleware.js');
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
@@ -15,6 +16,11 @@ const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```snippets')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.snippets = brew.text.slice(11, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
const printCurrentBrew = ()=>{
|
||||
@@ -28,7 +34,24 @@ const printCurrentBrew = ()=>{
|
||||
}
|
||||
};
|
||||
|
||||
const fetchThemeBundle = async (obj, renderer, theme)=>{
|
||||
const res = await request
|
||||
.get(`/api/theme/${renderer}/${theme}`)
|
||||
.catch((err)=>{
|
||||
obj.setState({ error: err });
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
const themeBundle = res.body;
|
||||
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
||||
obj.setState((prevState)=>({
|
||||
...prevState,
|
||||
themeBundle : themeBundle
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
splitTextStyleAndMetadata,
|
||||
printCurrentBrew
|
||||
printCurrentBrew,
|
||||
fetchThemeBundle,
|
||||
};
|
||||
|
||||
@@ -4,12 +4,12 @@ const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
||||
});
|
||||
|
||||
test('Check markdown is using the custom renderer; specifically that it adds target=_self attribute to internal links in HTML blocks', function() {
|
||||
const source = '<div>[Has _self Attribute?](#p1)</div>';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<div> <p><a href="#p1" target="_self">Has _self Attribute?</a></p>\n </div>');
|
||||
});
|
||||
|
||||
@@ -5,19 +5,19 @@ const Markdown = require('naturalcrit/markdown.js');
|
||||
describe('Inline Definition Lists', ()=>{
|
||||
test('No Term 1 Definition', function() {
|
||||
const source = ':: My First Definition\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt></dt><dd>My First Definition</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Single Definition Term', function() {
|
||||
const source = 'My term :: My First Definition\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>My term</dt><dd>My First Definition</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Multiple Definition Terms', function() {
|
||||
const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Definition of Term 1</dd>\n<dt>Term 2</dt><dd>Definition of Term 2</dd>\n</dl>');
|
||||
});
|
||||
});
|
||||
@@ -25,67 +25,67 @@ describe('Inline Definition Lists', ()=>{
|
||||
describe('Multiline Definition Lists', ()=>{
|
||||
test('Single Term, Single Definition', function() {
|
||||
const source = 'Term 1\n::Definition 1\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd></dl>');
|
||||
});
|
||||
|
||||
test('Single Term, Plural Definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\n::Definition 2\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single Definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Plural Definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||
});
|
||||
|
||||
test('Single Term, Single multi-line definition', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more and more</dd></dl>');
|
||||
});
|
||||
|
||||
test('Single Term, Plural multi-line definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dd>Definition 2 and more and more</dd>\n<dd>Definition 3</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single multi-line definition', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single multi-line definition, followed by an inline dl', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><dl><dt></dt><dd>Inline Definition (no term)</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single multi-line definition, followed by paragraph', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><p>Paragraph</p>');
|
||||
});
|
||||
|
||||
test('Block Token cannot be the Term of a multi-line definition', function() {
|
||||
const source = '## Header\n::Definition 1 of a single-line DL\n::Definition 1 of another single-line DL';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 id="header">Header</h2>\n<dl><dt></dt><dd>Definition 1 of a single-line DL</dd>\n<dt></dt><dd>Definition 1 of another single-line DL</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Inline DL has priority over Multiline', function() {
|
||||
const source = 'Term 1 :: Inline definition 1\n:: Inline definition 2 (no DT)';
|
||||
const rendered = Markdown.render(source, 0).trim();
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,37 +12,37 @@ const emoji = 'df_d12_2';
|
||||
describe(`When emojis/icons are active`, ()=>{
|
||||
it('when a word is between two colons (:word:), and a matching emoji exists, it is rendered as an emoji', function() {
|
||||
const source = `:${emoji}:`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><i class="df d12-2"></i></p>`);
|
||||
});
|
||||
|
||||
it('when a word is between two colons (:word:), and no matching emoji exists, it is not parsed', function() {
|
||||
const source = `:invalid:`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>:invalid:</p>`);
|
||||
});
|
||||
|
||||
it('two valid emojis with no whitespace are prioritized over definition lists', function() {
|
||||
const source = `:${emoji}::${emoji}:`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><i class="df d12-2"></i><i class="df d12-2"></i></p>`);
|
||||
});
|
||||
|
||||
it('definition lists that are not also part of an emoji can coexist with normal emojis', function() {
|
||||
const source = `definition :: term ${emoji}::${emoji}:`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>definition</dt><dd>term df_d12_2:<i class="df d12-2"></i></dd></dl>`);
|
||||
});
|
||||
|
||||
it('A valid emoji is compatible with curly injectors', function() {
|
||||
const source = `:${emoji}:{color:blue,myClass}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><i class="df d12-2 myClass" style="color:blue;"></i></p>`);
|
||||
});
|
||||
|
||||
it('Emojis are not parsed inside of curly span CSS blocks', function() {
|
||||
const source = `{{color:${emoji} text}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="color:df_d12_2;">text</span>`);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ describe(`When emojis/icons are active`, ()=>{
|
||||
const source = dedent`{{color:${emoji}
|
||||
text
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:df_d12_2;"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,112 +15,112 @@ String.prototype.trimReturns = function(){
|
||||
describe('Inline: When using the Inline syntax {{ }}', ()=>{
|
||||
it('Renders a mustache span with text only', function() {
|
||||
const source = '{{ text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text only, but with spaces', function() {
|
||||
const source = '{{ this is a text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
|
||||
});
|
||||
|
||||
it('Renders an empty mustache span', function() {
|
||||
const source = '{{}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with just a space', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with a few spaces only', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and class', function() {
|
||||
const source = '{{my-class text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and two classes', function() {
|
||||
const source = '{{my-class,my-class2 text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text with spaces and class', function() {
|
||||
const source = '{{my-class this is a text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and id', function() {
|
||||
const source = '{{#my-span text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and two ids', function() {
|
||||
const source = '{{#my-span,#my-favorite-span text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and css property', function() {
|
||||
const source = '{{color:red text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and two css properties', function() {
|
||||
const source = '{{color:red,padding:5px text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and css property which contains quotes', function() {
|
||||
const source = '{{font-family:"trebuchet ms" text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
|
||||
});
|
||||
|
||||
|
||||
it('Renders a mustache span with text with quotes and css property which contains double quotes', function() {
|
||||
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
|
||||
});
|
||||
|
||||
|
||||
it('Renders a mustache span with text with quotes and css property which contains double and simple quotes', function() {
|
||||
const source = `{{--stringVariable:"'string'" text "with quotes"}}`;
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text “with quotes”</span>`);
|
||||
});
|
||||
|
||||
|
||||
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
||||
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span with added attributes', function() {
|
||||
const source = 'Text and {{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e, text}} and more text!';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Text and <span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e">text</span> and more text!</p>\n');
|
||||
});
|
||||
});
|
||||
@@ -132,7 +132,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{
|
||||
text
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
@@ -140,14 +140,14 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{
|
||||
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
|
||||
});
|
||||
|
||||
it('Renders a single paragraph with opening and closing brackets', function() {
|
||||
const source = dedent`{{
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{cat
|
||||
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{cat
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{cat,dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -179,7 +179,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{--stringVariable:"'string'"
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{--stringVariable:"'string'"
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -203,7 +203,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{cat,color:red
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -211,7 +211,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{color:red,cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -219,7 +219,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{#cat,#dog
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
@@ -227,13 +227,13 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
|
||||
const source = dedent`{{color:red,cat,#dog,a="b and c",d="e"
|
||||
Sample text.
|
||||
}}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class=\"block cat\" id=\"dog\" style=\"color:red;\" a=\"b and c\" d=\"e\"><p>Sample text.</p></div>`);
|
||||
});
|
||||
|
||||
it('Renders a div with added attributes', function() {
|
||||
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e\nText and text and more text!\n}}\n';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e"><p>Text and text and more text!</p>\n</div>');
|
||||
});
|
||||
});
|
||||
@@ -245,116 +245,116 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
describe('and that element is an inline-block', ()=>{
|
||||
it('Renders a span "text" with no injection', function() {
|
||||
const source = '{{ text}}{}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with injected Class name', function() {
|
||||
const source = '{{ text}}{ClassName}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with injected attribute', function() {
|
||||
const source = '{{ text}}{a="b and c"}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" a="b and c">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with injected style', function() {
|
||||
const source = '{{ text}}{color:red}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with injected style using a string variable', function() {
|
||||
const source = `{{ text}}{--stringVariable:"'string'"}`;
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text</span>`);
|
||||
});
|
||||
|
||||
it('Renders a span "text" with two injected styles', function() {
|
||||
const source = '{{ text}}{color:red,background:blue}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
|
||||
const source = '{{#oldId text}}{#newId}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="newId">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
|
||||
const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new"}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
|
||||
const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new",class="new",style="new",id="new"}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own styles, appended with injected styles', function() {
|
||||
const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:blue; height:10px; width:10px; color:red;">text</span>');
|
||||
});
|
||||
|
||||
it('Renders a span "text" with its own classes, appended with injected classes', function() {
|
||||
const source = '{{classA,classB text}}{classA,classC}';
|
||||
const rendered = Markdown.render(source, 0);
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block classA classB classA classC">text</span>');
|
||||
});
|
||||
|
||||
it('Renders an emphasis element with injected Class name', function() {
|
||||
const source = '*emphasis*{big}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>');
|
||||
});
|
||||
|
||||
it('Renders a code element with injected style', function() {
|
||||
const source = '`code`{background:gray}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>');
|
||||
});
|
||||
|
||||
it('Renders an image element with injected style', function() {
|
||||
const source = '{position:absolute}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="position:absolute;" src="http://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
|
||||
});
|
||||
|
||||
it('Renders an element modified by only the first of two consecutive injections', function() {
|
||||
const source = '{{ text}}{color:red}{background:blue}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
||||
});
|
||||
|
||||
it('Renders an parent and child element, each modified by an injector', function() {
|
||||
const source = dedent`**bolded text**{color:red}
|
||||
{color:blue}`;
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
const rendered = Markdown.render(source, 1).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p style="color:blue;"><strong style="color:red;">bolded text</strong></p>');
|
||||
});
|
||||
|
||||
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 rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="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>`);
|
||||
});
|
||||
|
||||
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 rendered = Markdown.render(source).trimReturns();
|
||||
const rendered = Markdown.render(source, 1).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="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>`);
|
||||
});
|
||||
|
||||
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 rendered = Markdown.render(source).trimReturns();
|
||||
const rendered = Markdown.render(source, 1).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="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>`);
|
||||
});
|
||||
});
|
||||
@@ -362,19 +362,19 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
describe('and that element is a block', ()=>{
|
||||
it('renders a div "text" with no injection', function() {
|
||||
const source = '{{\ntext\n}}\n{}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('renders a div "text" with injected Class name', function() {
|
||||
const source = '{{\ntext\n}}\n{ClassName}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('renders a div "text" with injected style', function() {
|
||||
const source = '{{\ntext\n}}\n{color:red}';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
@@ -383,7 +383,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{color:red,background:blue}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue;"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
@@ -392,7 +392,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{--stringVariable:"'string'"}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>text</p></div>`);
|
||||
});
|
||||
|
||||
@@ -401,7 +401,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{#newId}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" id="newId"><p>text</p></div>');
|
||||
});
|
||||
|
||||
@@ -410,7 +410,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{attrA="new",attrC="new"}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
|
||||
});
|
||||
|
||||
@@ -419,7 +419,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{attrA="new",attrC="new",class="new",style="new",id="new"}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
|
||||
});
|
||||
|
||||
@@ -428,7 +428,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{width:10px,color:red}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:blue; height:10px; width:10px; color:red;"><p>text</p></div>');
|
||||
});
|
||||
|
||||
@@ -437,14 +437,14 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
text
|
||||
}}
|
||||
{classA,classC}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block classA classB classA classC"><p>text</p></div>');
|
||||
});
|
||||
|
||||
it('renders an h2 header "text" with injected class name', function() {
|
||||
const source = dedent`## text
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2>');
|
||||
});
|
||||
|
||||
@@ -455,7 +455,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
| 300 | 2 |
|
||||
|
||||
{ClassName}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<table class="ClassName"><thead><tr><th align=left>Experience Points</th><th align=center>Level</th></tr></thead><tbody><tr><td align=left>0</td><td align=center>1</td></tr><tr><td align=left>300</td><td align=center>2</td></tr></tbody></table>`);
|
||||
});
|
||||
|
||||
@@ -466,7 +466,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
// - Dark Chant of the Dentists
|
||||
// - Divine Spell of Crossdressing
|
||||
// {color:red}`;
|
||||
// const rendered = Markdown.render(source, 0).trimReturns();
|
||||
// const rendered = Markdown.render(source).trimReturns();
|
||||
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into <ul>? Currently injects into last <li>
|
||||
// });
|
||||
|
||||
@@ -474,7 +474,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
const source = dedent`## text
|
||||
{ClassName}
|
||||
{secondInjection}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2><p>{secondInjection}</p>');
|
||||
});
|
||||
|
||||
@@ -487,7 +487,7 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
||||
{innerDiv}
|
||||
}}
|
||||
{outerDiv}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block outerDiv"><p>outer text</p><div class="block innerDiv"><p>inner text</p></div></div>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Block-level variables', ()=>{
|
||||
|
||||
$[var]
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('Block-level variables', ()=>{
|
||||
lines
|
||||
|
||||
$[var]`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string across multiple lines</p>');
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('Block-level variables', ()=>{
|
||||
| C | D |
|
||||
|
||||
$[var]`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<h5 id="title">Title</h5>
|
||||
<table><thead><tr><th align=left>H1</th>
|
||||
@@ -71,7 +71,7 @@ describe('Block-level variables', ()=>{
|
||||
$[var]
|
||||
|
||||
[var]: string`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('Block-level variables', ()=>{
|
||||
[var]: string
|
||||
|
||||
[var]: new string`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>new string</p>');
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('Block-level variables', ()=>{
|
||||
|
||||
[lastName]: $[lastName]son
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Welcome, Mr. Bob Jacobson!</p>');
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('Block-level variables', ()=>{
|
||||
|
||||
$[var]
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>one</p><p>two</p>'.trimReturns());
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('Block-level variables', ()=>{
|
||||
|
||||
$[var]
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>two</p><p>one</p><p>two</p>'.trimReturns());
|
||||
});
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Block-level variables', ()=>{
|
||||
|
||||
$[last]: Jones
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is $[first] Jones</p>`.trimReturns());
|
||||
});
|
||||
});
|
||||
@@ -154,7 +154,7 @@ describe('Inline-level variables', ()=>{
|
||||
|
||||
$[var]
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p><p>string</p>');
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('Inline-level variables', ()=>{
|
||||
$[var](My name is $[name] Jones)
|
||||
|
||||
[name]: Bob`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>My name is Bob Jones</p>');
|
||||
});
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('Inline-level variables', ()=>{
|
||||
$[name](Bob)
|
||||
|
||||
[name]: Bill`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is Bill Jones</p> <p>Bob</p>`.trimReturns());
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('Inline-level variables', ()=>{
|
||||
$[var2](A variable ) with unbalanced parens)
|
||||
|
||||
$[var2]`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p>A variable (with nested parens) inside</p>
|
||||
<p>A variable (with nested parens) inside</p>
|
||||
@@ -202,35 +202,35 @@ describe('Math', ()=>{
|
||||
const source = dedent`
|
||||
$[1 + 3 * 5 - (1 / 4)]
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>15.75</p>');
|
||||
});
|
||||
|
||||
it('Handles round function', function() {
|
||||
const source = dedent`
|
||||
$[round(1/4)]`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
|
||||
});
|
||||
|
||||
it('Handles floor function', function() {
|
||||
const source = dedent`
|
||||
$[floor(0.6)]`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
|
||||
});
|
||||
|
||||
it('Handles ceil function', function() {
|
||||
const source = dedent`
|
||||
$[ceil(0.2)]`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
|
||||
});
|
||||
|
||||
it('Handles nested functions', function() {
|
||||
const source = dedent`
|
||||
$[ceil(floor(round(0.6)))]`;
|
||||
const rendered = Markdown.render(source, 0).replace(/\s/g, ' ').trimReturns();
|
||||
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
|
||||
});
|
||||
|
||||
@@ -242,7 +242,7 @@ describe('Math', ()=>{
|
||||
|
||||
Answer is $[answer]($[1 + 3 * num1 - (1 / num2)]).
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Answer is 15.75.</p>');
|
||||
});
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('Math', ()=>{
|
||||
|
||||
Increment num1 to get $[num1]($[num1 + 1]) and again to $[num1]($[num1 + 1]).
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Increment num1 to get 6 and again to 7.</p>');
|
||||
});
|
||||
});
|
||||
@@ -268,7 +268,7 @@ describe('Code blocks', ()=>{
|
||||
$[var](new string)
|
||||
\`\`\`
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<pre><code>
|
||||
[var]: string
|
||||
@@ -289,7 +289,7 @@ describe('Code blocks', ()=>{
|
||||
|
||||
$[var](new string)
|
||||
`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p>test</p>
|
||||
|
||||
@@ -304,7 +304,7 @@ describe('Code blocks', ()=>{
|
||||
|
||||
it('Ignores all variables in inline code blocks', function() {
|
||||
const source = '[var](Hello) `[link](url)`. This `[var] does not work`';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p><a href="Hello">var</a> <code>[link](url)</code>. This <code>[var] does not work</code></p>`.trimReturns());
|
||||
});
|
||||
@@ -313,35 +313,35 @@ describe('Code blocks', ()=>{
|
||||
describe('Normal Links and Images', ()=>{
|
||||
it('Renders normal images', function() {
|
||||
const source = ``;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p><img src="url" alt="alt text"></p>`.trimReturns());
|
||||
});
|
||||
|
||||
it('Renders normal images with a title', function() {
|
||||
const source = 'An image !';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns());
|
||||
});
|
||||
|
||||
it('Applies curly injectors to images', function() {
|
||||
const source = `{width:100px}`;
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
||||
});
|
||||
|
||||
it('Renders normal links', function() {
|
||||
const source = 'A Link to my [website](url)!';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p>A Link to my <a href="url">website</a>!</p>`.trimReturns());
|
||||
});
|
||||
|
||||
it('Renders normal links with a title', function() {
|
||||
const source = 'A Link to my [website](url "and title")!';
|
||||
const rendered = Markdown.render(source, 0).trimReturns();
|
||||
const rendered = Markdown.render(source).trimReturns();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||
<p>A Link to my <a href="url" title="and title">website</a>!</p>`.trimReturns());
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name" : "5e PHB",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseTheme" : "Blank",
|
||||
"baseSnippets" : false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name" : "Journal",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseTheme" : "Blank",
|
||||
"baseSnippets" : "5ePHB"
|
||||
}
|
||||
|
||||
@@ -74,8 +74,9 @@
|
||||
@font-face {
|
||||
font-family: SolberaImitationRemake; //Tweaked 5e version
|
||||
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
|
||||
font-weight: normal;
|
||||
font-weight: 100 1000;
|
||||
font-style: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Cover Page */
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"5ePHB": {
|
||||
"name": "5e PHB",
|
||||
"renderer": "V3",
|
||||
"baseTheme": false,
|
||||
"baseTheme": "Blank",
|
||||
"baseSnippets": false,
|
||||
"path": "5ePHB"
|
||||
},
|
||||
@@ -32,7 +32,7 @@
|
||||
"Journal": {
|
||||
"name": "Journal",
|
||||
"renderer": "V3",
|
||||
"baseTheme": false,
|
||||
"baseTheme": "Blank",
|
||||
"baseSnippets": "5ePHB",
|
||||
"path": "Journal"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user