Merge remote-tracking branch 'upstream/master'
@@ -85,14 +85,40 @@ pre {
|
||||
}
|
||||
|
||||
.page .df {
|
||||
font-size: 2em;
|
||||
vertical-align: middle;
|
||||
font-size: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
```
|
||||
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Saturday 4/04/2026 - v3.21.0
|
||||
|
||||
{{taskList
|
||||
##### Gazook89
|
||||
* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} to be inserted mid-line
|
||||
|
||||
##### abquintic
|
||||
* [x] Move example snippet images out of imgur (for folks without imgur access)
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Add auto-suggest to tag entry input box
|
||||
* [x] Replace all example artwork with
|
||||
* [x] Added tooltips to the {{openSans :fas_circle_info: **Properties**}} menu
|
||||
* [x] Removed {{openSans **SYSTEMS**}} checkboxes from {{openSans :fas_circle_info: **Properties**}} menu; instead {{openSans **TAGS**}} should be used for this purpose
|
||||
* [x] Replace all AI-generated art with public domain art
|
||||
* [x] Major backend refactor to use Vite
|
||||
|
||||
##### A1Asriel (new contributor!)
|
||||
* [x] Add fix for column breaks on Firefox
|
||||
|
||||
Fixes issues [#543](https://github.com/naturalcrit/homebrewery/issues/543), [#2473](https://github.com/naturalcrit/homebrewery/issues/2473), [#3712](https://github.com/naturalcrit/homebrewery/issues/3712)
|
||||
|
||||
##### G-Ambatte, abquintic, 5e-Cleric
|
||||
* [x] Multiple other backend fixes and refactors
|
||||
}}
|
||||
|
||||
### Friday 1/11/2026 - v3.20.1
|
||||
|
||||
{{taskList
|
||||
@@ -2358,4 +2384,4 @@ Massive changelog incoming:
|
||||
|
||||
* Added `phb.standalone.css` plus a build system for creating it
|
||||
* Added page numbers and footer text
|
||||
* Page accent now flips each page
|
||||
* Page accent now flips each page
|
||||
@@ -16,6 +16,7 @@ const CodeEditor = createReactClass({
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
onReady : ()=>{},
|
||||
enableFolding : true,
|
||||
editorTheme : 'default'
|
||||
};
|
||||
@@ -177,7 +178,7 @@ const CodeEditor = createReactClass({
|
||||
// return el;
|
||||
// }
|
||||
});
|
||||
|
||||
this.props.onReady?.(this.codeMirror);
|
||||
// Add custom behaviors (auto-close curlies and auto-complete emojis)
|
||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
|
||||
@@ -189,7 +190,8 @@ const CodeEditor = createReactClass({
|
||||
|
||||
// Use for GFM tabs that use common hot-keys
|
||||
isGFM : function() {
|
||||
if((this.isGFM()) || (this.props.tab === 'brewSnippets')) return true;
|
||||
console.log(this.props.tab);
|
||||
if( this.props.tab === 'brewText' || this.props.tab === 'brewSnippets') return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -226,7 +228,9 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeBold : function() {
|
||||
console.log('hello');
|
||||
if(!this.isGFM()) return;
|
||||
console.log(this.isGFM());
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
|
||||
if(selection.length === 0){
|
||||
|
||||
@@ -33,7 +33,7 @@ const INITIAL_CONTENT = dedent`
|
||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
|
||||
<link href="${headerNavStylesUrl}" rel="stylesheet" />
|
||||
<base target=_blank>
|
||||
<base target="_top">
|
||||
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||
|
||||
|
||||
@@ -272,6 +272,13 @@ const BrewRenderer = (props)=>{
|
||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||
scrollToHash(window.location.hash);
|
||||
|
||||
navigation.addEventListener('navigate', (e)=>{
|
||||
if(e.hashChange && e.destination.sameDocument){
|
||||
const dest = e.destination.url.slice(e.destination.url.indexOf('#'));
|
||||
scrollToHash(dest);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||
renderPages(); //Make sure page is renderable before showing
|
||||
setState((prevState)=>({
|
||||
|
||||
@@ -104,7 +104,7 @@ const HeaderNavItem = ({ link, text, depth, className })=>{
|
||||
if(!link || !text) return;
|
||||
|
||||
return <li>
|
||||
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
|
||||
<a href={`#${link}`} className={`depth-${depth} ${className ?? ''}`}>
|
||||
{trimString(text, depth)}
|
||||
</a>
|
||||
</li>;
|
||||
|
||||
@@ -76,9 +76,6 @@ const Editor = createReactClass({
|
||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
this.codeEditor.current.codeMirror?.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
|
||||
this.codeEditor.current.codeMirror?.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
|
||||
|
||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||
if(editorTheme) {
|
||||
this.setState({
|
||||
@@ -436,6 +433,29 @@ const Editor = createReactClass({
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
//temporary fix until cm6 comes next update
|
||||
attachCodeMirrorListeners : function(cm) {
|
||||
if(!cm) return;
|
||||
// detach previous (important on remount / view switch)
|
||||
if(this._cm) {
|
||||
this._cm.off('cursorActivity', this._onCursor);
|
||||
this._cm.off('scroll', this._onScroll);
|
||||
}
|
||||
|
||||
this._cm = cm;
|
||||
|
||||
this._onCursor = ()=>{
|
||||
this.updateCurrentCursorPage(cm.getCursor());
|
||||
};
|
||||
|
||||
this._onScroll = _.throttle(()=>{
|
||||
const topLine = cm.lineAtHeight(cm.getScrollInfo().top, 'local');
|
||||
this.updateCurrentViewPage(topLine);
|
||||
}, 200);
|
||||
|
||||
cm.on('cursorActivity', this._onCursor);
|
||||
cm.on('scroll', this._onScroll);
|
||||
},
|
||||
renderEditor : function(){
|
||||
if(this.isText()){
|
||||
return <>
|
||||
@@ -448,7 +468,8 @@ const Editor = createReactClass({
|
||||
onChange={this.props.onBrewChange('text')}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
|
||||
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}
|
||||
onReady={this.attachCodeMirrorListeners}/>
|
||||
</>;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
@@ -463,7 +484,8 @@ const Editor = createReactClass({
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
|
||||
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}
|
||||
onReady={this.attachCodeMirrorListeners}/>
|
||||
</>;
|
||||
}
|
||||
if(this.isMeta()){
|
||||
@@ -493,7 +515,8 @@ const Editor = createReactClass({
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} />
|
||||
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }}
|
||||
onReady={this.attachCodeMirrorListeners}/>
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default [
|
||||
export const tagSuggestionList = [
|
||||
// ############################## Systems
|
||||
// D&D
|
||||
'system:D&D Original',
|
||||
@@ -208,3 +208,12 @@ export default [
|
||||
'SW5e',
|
||||
'Star Wars 5e',
|
||||
];
|
||||
|
||||
// substrings to be normalized to the first value on the array
|
||||
export const canonizationList = [
|
||||
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
|
||||
['5e', '5th Edition'],
|
||||
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
|
||||
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
|
||||
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
|
||||
];
|
||||
@@ -2,7 +2,7 @@ import './tagInput.less';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Combobox from '../../../components/combobox.jsx';
|
||||
|
||||
import tagSuggestionList from './curatedTagSuggestionList.js';
|
||||
import { tagSuggestionList, canonizationList } from './curatedTagSuggestionList.js';
|
||||
|
||||
const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
|
||||
const [tagList, setTagList] = useState(
|
||||
@@ -35,20 +35,11 @@ const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, p
|
||||
});
|
||||
}, [tagList]);
|
||||
|
||||
// substrings to be normalized to the first value on the array
|
||||
const duplicateGroups = [
|
||||
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
|
||||
['5e', '5th Edition'],
|
||||
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
|
||||
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
|
||||
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
|
||||
];
|
||||
|
||||
const normalizeValue = (input)=>{
|
||||
const lowerInput = input.toLowerCase();
|
||||
let normalizedTag = input;
|
||||
|
||||
for (const group of duplicateGroups) {
|
||||
for (const group of canonizationList) {
|
||||
for (const tag of group) {
|
||||
if(!tag) continue;
|
||||
|
||||
|
||||
@@ -52,6 +52,19 @@ const Homebrew = (props)=>{
|
||||
|
||||
updateLocalStorage();
|
||||
|
||||
if(brew.pureError) {
|
||||
return (
|
||||
<Router>
|
||||
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||
<Routes>
|
||||
<Route path={brew.originalUrl} element={<WithRoute el={ErrorPage} brew={brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||
|
||||
@@ -46,11 +46,6 @@ const MetadataNav = createReactClass({
|
||||
</>;
|
||||
},
|
||||
|
||||
getSystems : function(){
|
||||
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
|
||||
return this.props.brew.systems.join(', ');
|
||||
},
|
||||
|
||||
renderMetaWindow : function(){
|
||||
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
|
||||
<div className='row'>
|
||||
@@ -65,10 +60,6 @@ const MetadataNav = createReactClass({
|
||||
<h4>Tags</h4>
|
||||
<p>{this.getTags()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Systems</h4>
|
||||
<p>{this.getSystems()}</p>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<h4>Updated</h4>
|
||||
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
|
||||
|
||||
@@ -24,7 +24,7 @@ const NewBrew = ()=>{
|
||||
localStorage.setItem(BREWKEY, newBrew.text);
|
||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify(
|
||||
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
|
||||
_.pick(newBrew, ['title', 'description', 'tags', 'renderer', 'theme', 'lang'])
|
||||
));
|
||||
window.location.href = '/new';
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.uiPage.sitePage:has(.errorTitle) {
|
||||
.errorTitle {
|
||||
//background-color: @orange;
|
||||
color : #D02727;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,28 @@
|
||||
<main id="reactRoot"></main>
|
||||
|
||||
<script type="module">
|
||||
if (window.location.pathname.startsWith('/admin')) {
|
||||
const props = window.__INITIAL_PROPS__ || {};
|
||||
const url = props.config?.baseUrl;
|
||||
const title = props.brew?.title;
|
||||
|
||||
let prefix = '';
|
||||
|
||||
if (url && url?.includes('://homebrewery-stage.')) {
|
||||
prefix = `Stage `;
|
||||
} else if (url?.includes('://homebrewery-pr-')) {
|
||||
const match = url.match(/pr-(\d+)/);
|
||||
if (match) prefix = `PR-${match[1]} `;
|
||||
} else if (url?.includes('://localhost')) {
|
||||
prefix = 'Local ';
|
||||
}
|
||||
|
||||
if (title) {
|
||||
document.title = `${prefix} - ${title} - The Homebrewery`;
|
||||
} else if (prefix) {
|
||||
document.title = `${prefix} - The Homebrewery`;
|
||||
}
|
||||
|
||||
if (window.location.pathname.startsWith('/admin')) {
|
||||
import('/client/admin/main.jsx');
|
||||
} else {
|
||||
import('/client/homebrew/main.jsx');
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.20.1",
|
||||
"version": "3.21.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": "^10.8.x",
|
||||
"node": "^20.18.x"
|
||||
"npm": ">=10.8 <12",
|
||||
"node": ">=20.18 <25"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -88,9 +88,9 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||
"@googleapis/drive": "^20.1.0",
|
||||
"@sanity/diff-match-patch": "^3.2.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"core-js": "^3.47.0",
|
||||
"core-js": "^3.49.0",
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent": "^1.7.1",
|
||||
@@ -112,8 +112,8 @@
|
||||
"idb-keyval": "^6.2.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^4.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"less": "^4.6.4",
|
||||
"lodash": "^4.18.1",
|
||||
"marked": "15.0.12",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
@@ -126,32 +126,32 @@
|
||||
"marked-variables": "^1.0.5",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^9.2.1",
|
||||
"nanoid": "5.1.6",
|
||||
"mongoose": "^9.3.3",
|
||||
"nanoid": "5.1.7",
|
||||
"nconf": "^0.13.0",
|
||||
"node": "^25.7.0",
|
||||
"node": "^25.9.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-frame-component": "^5.2.7",
|
||||
"react-router": "^7.13.1",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"react-frame-component": "^5.3.2",
|
||||
"react-router": "^7.14.0",
|
||||
"sanitize-filename": "1.6.4",
|
||||
"superagent": "^10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^5.0.1",
|
||||
"babel-jest": "^30.2.0",
|
||||
"babel-jest": "^30.3.0",
|
||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-jest": "^29.1.0",
|
||||
"eslint": "9.7",
|
||||
"eslint-plugin-jest": "^29.15.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom": "^28.1.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^17.4.0",
|
||||
"stylelint-config-recess-order": "^7.6.1",
|
||||
"stylelint": "^17.6.0",
|
||||
"stylelint-config-recess-order": "^7.7.0",
|
||||
"stylelint-config-recommended": "^18.0.0",
|
||||
"supertest": "^7.1.4",
|
||||
"vite": "^7.3.1"
|
||||
|
||||
@@ -14,7 +14,6 @@ const DEFAULT_BREW = {
|
||||
theme : '5ePHB',
|
||||
authors : [],
|
||||
tags : [],
|
||||
systems : [],
|
||||
lang : 'en',
|
||||
thumbnail : '',
|
||||
views : 0,
|
||||
|
||||
@@ -151,7 +151,6 @@ const GoogleActions = {
|
||||
description : file.description,
|
||||
views : parseInt(file.properties.views),
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
systems : [],
|
||||
lang : file.properties.lang,
|
||||
thumbnail : file.properties.thumbnail,
|
||||
webViewLink : file.webViewLink
|
||||
@@ -298,7 +297,6 @@ const GoogleActions = {
|
||||
text : file.data,
|
||||
|
||||
description : obj.data.description,
|
||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||
authors : [],
|
||||
lang : obj.data.properties.lang,
|
||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||
|
||||
@@ -31,6 +31,27 @@ const isStaticTheme = (renderer, themeName)=>{
|
||||
// });
|
||||
// };
|
||||
|
||||
|
||||
const migrateSystemsToTags = (brew) => {
|
||||
if (!('systems' in brew)) return brew;
|
||||
|
||||
if (!Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||
brew.systems = undefined;
|
||||
return brew;
|
||||
}
|
||||
const systemMap = {
|
||||
'5e': 'system:D&D 5e',
|
||||
'4e': 'system:D&D 4e',
|
||||
'3.5e': 'system:D&D 3.5e',
|
||||
'Pathfinder': 'system:Pathfinder 2e'
|
||||
};
|
||||
const systemTags = brew.systems.map(s => systemMap[s]);
|
||||
brew.tags = _.uniq([...(brew.tags || []), ...systemTags]);
|
||||
|
||||
brew.systems = undefined;
|
||||
return brew;
|
||||
};
|
||||
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
const api = {
|
||||
@@ -167,7 +188,10 @@ const api = {
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
|
||||
req.brew = stub;
|
||||
|
||||
|
||||
const fixedStub = migrateSystemsToTags(stub);
|
||||
req.brew = fixedStub;
|
||||
next();
|
||||
};
|
||||
},
|
||||
@@ -193,7 +217,7 @@ const api = {
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']);
|
||||
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
|
||||
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
|
||||
text = `\`\`\`metadata\n` +
|
||||
@@ -392,6 +416,9 @@ const api = {
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
|
||||
migrateSystemsToTags(brew);
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
@@ -63,7 +63,6 @@ describe('Tests for api', ()=>{
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
lang : 'en',
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
@@ -351,7 +350,6 @@ describe('Tests for api', ()=>{
|
||||
renderer : 'legacy',
|
||||
lang : 'en',
|
||||
shareId : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
@@ -390,7 +388,6 @@ describe('Tests for api', ()=>{
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345'
|
||||
@@ -402,8 +399,6 @@ description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
systems:
|
||||
- D&D 5e
|
||||
renderer: v3
|
||||
theme: phb
|
||||
|
||||
@@ -419,7 +414,6 @@ brew`);
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345'
|
||||
@@ -431,8 +425,6 @@ description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
systems:
|
||||
- D&D 5e
|
||||
renderer: v3
|
||||
theme: phb
|
||||
|
||||
@@ -463,7 +455,6 @@ brew`);
|
||||
|
||||
expect(sent).toEqual(googleBrew);
|
||||
expect(result.tags).toBeUndefined();
|
||||
expect(result.systems).toBeUndefined();
|
||||
expect(result.published).toBeUndefined();
|
||||
expect(result.authors).toBeUndefined();
|
||||
expect(result.owner).toBeUndefined();
|
||||
@@ -558,7 +549,6 @@ brew`);
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : expect.objectContaining({}),
|
||||
@@ -618,7 +608,6 @@ brew`);
|
||||
shareId : expect.any(String),
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : undefined,
|
||||
@@ -1076,7 +1065,6 @@ brew`);
|
||||
'title: title\n' +
|
||||
'description: description\n' +
|
||||
'tags: [ \'tag a\' , \'tag b\' ]\n' +
|
||||
'systems: [ test system ]\n' +
|
||||
'renderer: legacy\n' +
|
||||
'theme: 5ePHB\n' +
|
||||
'lang: en\n' +
|
||||
@@ -1097,8 +1085,6 @@ brew`);
|
||||
// Metadata
|
||||
expect(testBrew.title).toEqual('title');
|
||||
expect(testBrew.description).toEqual('description');
|
||||
expect(testBrew.tags).toEqual(['tag a', 'tag b']);
|
||||
expect(testBrew.systems).toEqual(['test system']);
|
||||
expect(testBrew.renderer).toEqual('legacy');
|
||||
expect(testBrew.theme).toEqual('5ePHB');
|
||||
expect(testBrew.lang).toEqual('en');
|
||||
@@ -1107,19 +1093,6 @@ brew`);
|
||||
// Text
|
||||
expect(testBrew.text).toEqual('text\n');
|
||||
});
|
||||
|
||||
it('convert tags string to array', async ()=>{
|
||||
const testBrew = {
|
||||
text : '```metadata\n' +
|
||||
'tags: tag a\n' +
|
||||
'```\n\n'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(testBrew);
|
||||
|
||||
// Metadata
|
||||
expect(testBrew.tags).toEqual(['tag a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBrew', ()=>{
|
||||
|
||||
@@ -15,7 +15,7 @@ const HomebrewSchema = mongoose.Schema({
|
||||
|
||||
description : { type: String, default: '' },
|
||||
tags : { type: [String], index: true },
|
||||
systems : [String],
|
||||
systems : { type: [String], default: undefined },
|
||||
lang : { type: String, default: 'en', index: true },
|
||||
renderer : { type: String, default: '', index: true },
|
||||
authors : { type: [String], index: true },
|
||||
|
||||
@@ -91,7 +91,7 @@ const splitTextStyleAndMetadata = (brew)=>{
|
||||
const index = brew.text.indexOf('\n```\n\n');
|
||||
const metadataSection = brew.text.slice(11, index + 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'renderer', 'theme', 'lang']));
|
||||
brew.snippets = yamlSnippetsToText(_.pick(metadata, ['snippets']).snippets || '');
|
||||
brew.text = brew.text.slice(index + 6);
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ renderer.link = function (token) {
|
||||
if(title) {
|
||||
out += ` title="${escape(title)}"`;
|
||||
}
|
||||
if(self) {
|
||||
out += ' target="_self"';
|
||||
}
|
||||
// if(self) {
|
||||
// out += ' target="_self"';
|
||||
// }
|
||||
out += `>${text}</a>`;
|
||||
return out;
|
||||
};
|
||||
|
||||
@@ -34,9 +34,9 @@ renderer.link = function (href, title, text) {
|
||||
if(title) {
|
||||
out += ` title="${title}"`;
|
||||
}
|
||||
if(self) {
|
||||
out += ' target="_self"';
|
||||
}
|
||||
// if(self) {
|
||||
// out += ' target="_self"';
|
||||
// }
|
||||
out += `>${text}</a>`;
|
||||
return out;
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@ test('Processes the markdown within an HTML block if its just a class wrapper',
|
||||
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);
|
||||
expect(rendered).toBe('<div> <p><a href="#p1" target="_self">Has _self Attribute?</a></p>\n </div>');
|
||||
});
|
||||
// TEST REMOVED AS IT IS NO LONGER REQUIRED
|
||||
//
|
||||
// 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);
|
||||
// expect(rendered).toBe('<div> <p><a href="#p1" target="_self">Has _self Attribute?</a></p>\n </div>');
|
||||
// });
|
||||
|
||||
@@ -143,8 +143,8 @@ export default [
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{artist,top:90px,right:30px
|
||||
##### Starry Night
|
||||
[Van Gogh](https://www.vangoghmuseum.nl/en)
|
||||
##### Bird with autumn foliage
|
||||
[by L. Prang & Co.](https://www.loc.gov/resource/pga.14148)
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import dedent from 'dedent';
|
||||
const domain = window.location.origin;
|
||||
|
||||
const titles = [
|
||||
'The Burning Gallows', 'The Ring of Nenlast',
|
||||
@@ -84,7 +85,7 @@ export default {
|
||||
return dedent`
|
||||
{{frontCover}}
|
||||
|
||||
{{logo }}
|
||||
{{logo }}
|
||||
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
@@ -96,7 +97,7 @@ export default {
|
||||
${_.sample(footnote)}
|
||||
}}
|
||||
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,right:-400px,height:100%}
|
||||
|
||||
\page`;
|
||||
},
|
||||
@@ -110,10 +111,10 @@ export default {
|
||||
___
|
||||
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:100px,right:70px,height:70%}
|
||||
}}
|
||||
|
||||
{{logo }}
|
||||
{{logo }}
|
||||
|
||||
\page`;
|
||||
},
|
||||
@@ -126,7 +127,7 @@ export default {
|
||||
## ${_.sample(subtitles)}
|
||||
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:10cm,--rotation:180
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,right:0,height:100%}
|
||||
}}
|
||||
|
||||
\page`;
|
||||
@@ -143,10 +144,10 @@ export default {
|
||||
|
||||
For use with any fantasy roleplaying ruleset. Play the best game of your life!
|
||||
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,right:-250px,height:100%}
|
||||
|
||||
{{logo
|
||||

|
||||

|
||||
|
||||
Homebrewery.Naturalcrit.com
|
||||
}}`;
|
||||
|
||||
@@ -11,6 +11,7 @@ import LicenseDTTRPGGCC from './snippets/licenseDTRPGCC.gen.js';
|
||||
import LicenseMongoosePublishing from './snippets/licenseMongoose.gen.js';
|
||||
import TableOfContentsGen from './snippets/tableOfContents.gen.js';
|
||||
import indexGen from './snippets/index.gen.js';
|
||||
const domain = window.location.origin;
|
||||
|
||||
export default [
|
||||
|
||||
@@ -645,25 +646,25 @@ export default [
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}`
|
||||
 {width:325px}`
|
||||
},
|
||||
{
|
||||
name : 'Image Wrap Left',
|
||||
icon : 'fac image-wrap-left',
|
||||
gen : dedent`
|
||||
 {width:280px,margin-right:-3cm,wrapLeft}`
|
||||
 {width:280px,margin-right:-3cm,wrapLeft}`
|
||||
},
|
||||
{
|
||||
name : 'Image Wrap Right',
|
||||
icon : 'fac image-wrap-right',
|
||||
gen : dedent`
|
||||
 {width:280px,margin-left:-3cm,wrapRight}`
|
||||
 {width:280px,margin-left:-3cm,wrapRight}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}`
|
||||
},
|
||||
{
|
||||
name : 'Watercolor Splatter',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import dedent from 'dedent';
|
||||
const domain = window.location.origin;
|
||||
|
||||
export default {
|
||||
center : ()=>{
|
||||
return dedent`
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{height:100%}
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||
Use --offsetY to shift the mask up or down
|
||||
@@ -13,6 +14,20 @@ export default {
|
||||
},
|
||||
|
||||
edge : (side = 'bottom')=>{
|
||||
const styles = ()=>{
|
||||
switch (side) {
|
||||
case 'bottom':
|
||||
return `{width:100%,bottom:0%}`
|
||||
break;
|
||||
case 'top':
|
||||
return `{width:100%,top:0%}`
|
||||
break;
|
||||
default:
|
||||
return `{height:100%}`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const rotation = {
|
||||
'bottom' : 0,
|
||||
'top' : 180,
|
||||
@@ -20,8 +35,8 @@ export default {
|
||||
'right' : 270
|
||||
}[side];
|
||||
return dedent`
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:0%,--rotation:${rotation}
|
||||
{height:100%}
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:10%,--rotation:${rotation}
|
||||
${styles()}
|
||||
}}
|
||||
<!-- Use --offset to shift the mask away from page center (can use cm instead of %)
|
||||
Use --rotation to set rotation angle in degrees. -->\n\n`;
|
||||
@@ -32,7 +47,7 @@ export default {
|
||||
const offsetY = (y == 'top' ? '50%' : '-50%');
|
||||
return dedent`
|
||||
{{imageMaskCorner${_.random(1, 37)},--offsetX:${offsetX},--offsetY:${offsetY},--rotation:0
|
||||
{height:100%}
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||
Use --offsetY to shift the mask up or down
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import dedent from 'dedent';
|
||||
const domain = window.location.origin;
|
||||
|
||||
// Small and one-off licenses
|
||||
// Licenses in this file consist of one or two functions at most. If something is larger,
|
||||
@@ -50,10 +51,10 @@ export default {
|
||||
ccbyndBadge : ``,
|
||||
ccbyncndBadge : ``,
|
||||
shadowDarkNotice : `\[Product Name]\ is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.\n`,
|
||||
shadowDarkBlack : `{width:200px}`,
|
||||
shadowDarkWhite : `{width:200px}`,
|
||||
shadowDarkBlack : `{width:200px}`,
|
||||
shadowDarkWhite : `{width:200px}`,
|
||||
bladesDarkNotice : `This work is based on Blades in the Dark \(found at (http://www.bladesinthedark.com/)\), product of One Seven Design, developed and authored by John Harper, and licensed for our use under the Creative Commons Attribution 3.0 Unported license \(http://creativecommons.org/licenses/by/3.0/\).\n`,
|
||||
bladesDarkLogo : ``,
|
||||
bladesDarkLogo : ``,
|
||||
bladesDarkLogoAttribution : `*Blades in the Dark^tm^ is a trademark of One Seven Design. The Forged in the Dark Logo is © One Seven Design, and is used with permission.*`,
|
||||
iconsCompatibility : 'Compatibility with Icons requires Icons Superpowered Roleplaying from Ad Infinitum Adventures. Ad Infinitum Adventures does not guarantee compatibility, and does not endorse this product.',
|
||||
iconsTrademark : 'Icons Superpowered Roleplaying is a trademark of Steve Kenson, published exclusively by Ad Infinitum Adventures. The Icons Superpowered Roleplaying Compatibility Logo is a trademark of Ad Infinitum Adventures and is used under the Icons Superpowered Roleplaying Compatibility License.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import dedent from 'dedent';
|
||||
const domain = window.location.origin;
|
||||
|
||||
// DriveThruRPG/OneBookShelf Community Content Programs
|
||||
|
||||
@@ -101,10 +102,10 @@ export default {
|
||||
},
|
||||
// Verify Logo redistribution
|
||||
greenRoninAgeCreatorsAllianceCover : `Requires the \[Game Title\] Rulebook from Green Ronin Publishing for use.`,
|
||||
greenRoninAgeCreatorsAllianceLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceBlueRoseLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceFantasyAgeCompatible : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceModernAGECompatible : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceBlueRoseLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceFantasyAgeCompatible : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceModernAGECompatible : `{width:200px}`,
|
||||
// Green Ronin's Chronicle - Verify Art and Access
|
||||
greenRoninChronicleSystemGuildColophon : function() {
|
||||
return dedent`
|
||||
@@ -179,10 +180,10 @@ export default {
|
||||
`;
|
||||
},
|
||||
// Verify Logo redistribution
|
||||
monteCookLogoDarkLarge : ``,
|
||||
monteCookLogoDarkSmall : ``,
|
||||
monteCookLogoLightLarge : ``,
|
||||
monteCookLogoLightSmall : ``,
|
||||
monteCookLogoDarkLarge : ``,
|
||||
monteCookLogoDarkSmall : ``,
|
||||
monteCookLogoLightLarge : ``,
|
||||
monteCookLogoLightSmall : ``,
|
||||
// Onyx Path Canis Minor - Verify logos and access
|
||||
onyxPathCanisMinorColophon : function () {
|
||||
return dedent`
|
||||
|
||||
|
After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
@@ -33,26 +33,26 @@
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../fonts/5e/Nodesto Caps Condensed.woff2');
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../fonts/5e/Nodesto Caps Condensed Bold.woff2');
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../fonts/5e/Nodesto Caps Condensed Italic.woff2');
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Italic.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
|
||||
}
|
||||
|
||||