diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5effc0bb2..cc6bd8d82 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -82,6 +82,9 @@ jobs:
- run:
name: Test - HTML sanitization
command: npm run test:safehtml
+ - run:
+ name: Test - Helpers
+ command: npm run test:helpers
- run:
name: Test - Coverage
command: npm run test:coverage
diff --git a/changelog.md b/changelog.md
index 136f6346a..1c5a9b714 100644
--- a/changelog.md
+++ b/changelog.md
@@ -93,30 +93,83 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
-### Saturday 4/20/2026 - v3.22.0
-
+### Monday 9/07/2026 - v3.23.0
{{taskList
+##### firstmatekidd (new contributor!)
+* [x] Add nav buttons to tab order for accesibility
+* [x] Add title to brewRenderer iframe for accessibility
+* [x] Add aria labels to toolbars
+
+Partially fixes issue [#3032](https://github.com/naturalcrit/homebrewery/issues/3032)
+
+##### ReinaSweet (new contributor!)
+* [x] Added security to prevent scripts running in brews
+
+##### Gazook89
+* [x] Lazy load brew images for faster first load
+* [x] Fix snippet menus getting cut off behind divider bar
+
+Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840)
+
+##### Abquintic
+* [x] Fix variables not hoisting in later pages
+
##### 5e-Cleric
-* [x] Major update to editor framework (Codemirror 6)
-Fixes issues [#3511](https://github.com/naturalcrit/homebrewery/issues/3511), [#4590](https://github.com/naturalcrit/homebrewery/issues/4590), [#4563](https://github.com/naturalcrit/homebrewery/issues/4653), [#4655](https://github.com/naturalcrit/homebrewery/issues/4655)
-* [x] Fix to Admin page tab names
+* [x] Add image preview when hovering over image syntax
+
+Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037)
+
+* [x] Remember editor theme across tabs
+
+Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
+
+* [x] Add ability to remove co-authors if you are the owner
+
+Fixes part of issue [#4101](https://github.com/naturalcrit/homebrewery/issues/4101)
##### G-Ambatte
-* [x] Fix white page crash on certain browsers
+* [x] Fix editor panel shrinking when opening dev tools
+
+Fixes issue [#4866](https://github.com/naturalcrit/homebrewery/issues/4866)
+
+* [x] Added security to prevent scripts running in brews
+
+Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904)
+
+##### Abquintic, G-Ambatte, Gazook89, 5e-Cleric
+* [x] Multiple background fixes and code cleanups
}}
+### Wednesday 5/13/2026 - v3.22.1
+{{taskList
+##### Frederlk (new Contributor!)
+* [x] Add "share current page" to {{openSans **SHARE :fas_share_nodes:**}} menu
+
+##### Calculuschild, 5e-Cleric
+* [x] Fix brews triggering unneeded save when loaded
+
+##### G-Ambatte
+* [x] Fix brews with `$` in text getting out of sync on save
+
+##### 5e-Cleric, Gazook89
+* [x] Fix various issues with Codemirror 6
+
+Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4783](https://github.com/naturalcrit/homebrewery/issues/4783)
+}}
+
+\page
+
### Saturday 4/04/2026 - v3.21.0
{{taskList
##### Gazook89
-* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} to be inserted mid-line
+* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} dropdown 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
@@ -131,6 +184,7 @@ Fixes issues [#543](https://github.com/naturalcrit/homebrewery/issues/543), [#24
* [x] Multiple other backend fixes and refactors
}}
+
### Friday 1/11/2026 - v3.20.1
{{taskList
@@ -167,7 +221,7 @@ Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559)
##### G-Ambatte
* [x] Fix default save location failing on new documents
-Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/3175)
+Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/4437)
* [x] Fix usernames with special symbols unable to open userpage
Fixes issue [#807](https://github.com/naturalcrit/homebrewery/issues/807)
@@ -326,7 +380,7 @@ Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729)
Fixes issue [#4079](https://github.com/naturalcrit/homebrewery/issues/4079)
##### Calculuschild
-* [x] `꞉꞉꞉꞉` now produces `
` instead of a `
${text}`;
- res.status(200).send(text);
- });
-
- //Download brew source page
- app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
- const { brew } = req;
- sanitizeBrew(brew, 'share');
- const prefix = 'HB - ';
-
- const encodeRFC3986ValueChars = (str)=>{
- return (
- encodeURIComponent(str)
- .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
- );
- };
-
- let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
- if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
- res.set({
- 'Cache-Control' : 'no-cache',
- 'Content-Type' : 'text/plain',
- 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
- });
- res.status(200).send(brew.text);
- });
+ brew.authors.includes(req.account?.username)
+ ? sanitizeBrew(brew, 'shareAuthor')
+ : sanitizeBrew(brew, 'share');
+ splitTextStyleAndMetadata(brew);
+ res.json({ brew });
+ }));
//Serve brew metadata
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
@@ -280,78 +166,6 @@ export default async function createApp(vite) {
//Serve brew styling
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
- //User Page
- app.get('/user/:username', dbCheck, async (req, res, next)=>{
- const ownAccount = req.account && (req.account.username == req.params.username);
-
- req.ogMeta = { ...defaultMetaTags,
- title : `${req.params.username}'s Collection`,
- description : 'View my collection of homebrew on the Homebrewery.'
- // type : could be 'profile'?
- };
-
- const fields = [
- 'googleId',
- 'title',
- 'pageCount',
- 'description',
- 'authors',
- 'lang',
- 'published',
- 'views',
- 'shareId',
- 'editId',
- 'createdAt',
- 'updatedAt',
- 'lastViewed',
- 'thumbnail',
- 'tags'
- ];
-
- let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
- .catch((err)=>{
- console.log(err);
- });
-
- brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
-
- if(ownAccount && req?.account?.googleId){
- const auth = await GoogleActions.authCheck(req.account, res);
- let googleBrews = await GoogleActions.listGoogleBrews(auth)
- .catch((err)=>{
- console.error(err);
- });
-
- // If stub matches file from Google, use Google metadata over stub metadata
- if(googleBrews && googleBrews.length > 0) {
- for (const brew of brews.filter((brew)=>brew.googleId)) {
- const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
- if(match !== -1) {
- brew.googleId = googleBrews[match].googleId;
- brew.pageCount = googleBrews[match].pageCount;
- brew.renderer = googleBrews[match].renderer;
- brew.version = googleBrews[match].version;
- brew.webViewLink = googleBrews[match].webViewLink;
- googleBrews.splice(match, 1);
- }
- }
-
- //Remaining unstubbed google brews display current user as author
- googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
- brews = _.concat(brews, googleBrews);
- }
- }
-
- req.brews = _.map(brews, (brew)=>{
- // Clean up brew data
- brew.title = brew.title?.trim();
- brew.description = brew.description?.trim();
- return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
- });
-
- return next();
- });
-
//Change author name on brews
app.put('/api/user/rename', dbCheck, async (req, res)=>{
const { username, newUsername } = req.body;
@@ -380,143 +194,26 @@ export default async function createApp(vite) {
}
});
- //Edit Page
- app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
- req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
+ // Create Event Stream source for pages to listen to
+ app.get('/stream', (req, res)=>{
+ res.writeHead(200, {
+ 'Content-Type' : 'text/event-stream',
+ 'Cache-Control' : 'no-cache',
+ 'Connection' : 'keep-alive',
+ 'Content-Encoding' : 'none'
+ });
- req.userThemes = await(getUsersBrewThemes(req.account?.username));
+ Stream.on('sendUpdate', (event, data)=>{
+ console.log('Event:', event, '\nData:', data);
+ res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`);
+ });
+ });
- req.ogMeta = { ...defaultMetaTags,
- title : req.brew.title || 'Untitled Brew',
- description : req.brew.description || 'No description.',
- image : req.brew.thumbnail || defaultMetaTags.image,
- locale : req.brew.lang,
- type : 'article'
- };
+ // After Stream starts, send initStream event
+ setTimeout(()=>{
+ Stream.emit('sendUpdate', 'initStream', { time: new Date });
+ }, 1000);
- sanitizeBrew(req.brew, 'edit');
- 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 from ID
- app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
- sanitizeBrew(req.brew, 'share');
- splitTextStyleAndMetadata(req.brew);
- const brew = {
- shareId : req.brew.shareId,
- title : `CLONE - ${req.brew.title}`,
- text : req.brew.text,
- style : req.brew.style,
- renderer : req.brew.renderer,
- theme : req.brew.theme,
- tags : req.brew.tags,
- snippets : req.brew.snippets
- };
- 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', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
- const { brew } = req;
- req.ogMeta = { ...defaultMetaTags,
- title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
- description : req.brew.description || 'No description.',
- image : req.brew.thumbnail || defaultMetaTags.image,
- type : 'article'
- };
-
- // increase visitor view count, do not include visits by author(s)
- if(!brew.authors.includes(req.account?.username)){
- if(req.params.id.length > 12 && !brew._id) {
- const googleId = brew.googleId;
- const shareId = brew.shareId;
- await GoogleActions.increaseView(googleId, shareId, 'share', brew)
- .catch((err)=>{next(err);});
- } else {
- await HomebrewModel.increaseView({ shareId: brew.shareId });
- }
- };
-
- brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
- splitTextStyleAndMetadata(req.brew);
- return next();
- }));
-
- //Account Page
- app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
- const data = {};
- data.title = 'Account Information Page';
-
- if(!req.account) {
- res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
- const error = new Error('No valid account');
- error.status = 401;
- error.HBErrorCode = '50';
- error.page = data.title;
- return next(error);
- };
-
- let auth;
- let googleCount = [];
- if(req.account) {
- if(req.account.googleId) {
- auth = await GoogleActions.authCheck(req.account, res, false);
-
- googleCount = await GoogleActions.listGoogleBrews(auth)
- .catch((err)=>{
- console.error(err);
- });
- }
-
- const query = { authors: req.account.username, googleId: { $exists: false } };
- const mongoCount = await HomebrewModel.countDocuments(query)
- .catch((err)=>{
- console.log(err);
- return 0;
- });
-
- data.accountDetails = {
- username : req.account.username,
- issued : req.account.issued,
- googleId : Boolean(req.account.googleId),
- authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
- mongoCount : mongoCount,
- googleCount : googleCount?.length
- };
- }
-
- req.brew = data;
-
- req.ogMeta = { ...defaultMetaTags,
- title : `Account Page`,
- description : null
- };
-
- return next();
- }));
// Local only
if(isLocalEnvironment){
@@ -534,15 +231,6 @@ export default async function createApp(vite) {
app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
- //Vault Page
- app.get('/vault', asyncHandler(async(req, res, next)=>{
- req.ogMeta = { ...defaultMetaTags,
- title : 'The Vault',
- description : 'Search for Brews'
- };
- return next();
- }));
-
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js
new file mode 100644
index 000000000..66fdb3354
--- /dev/null
+++ b/server/eventStreamSource.js
@@ -0,0 +1,9 @@
+import { EventEmitter } from 'events';
+
+const Stream = new EventEmitter;
+
+export default {
+ emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions
+ on : (event, listener)=>{return Stream.on(event, listener);},
+ off : (event, listener)=>{return Stream.off(event, listener);}
+};
\ No newline at end of file
diff --git a/server/homebrew.api.js b/server/homebrew.api.js
index e9dea64b8..09b8b2677 100644
--- a/server/homebrew.api.js
+++ b/server/homebrew.api.js
@@ -4,7 +4,7 @@ import { model as HomebrewModel } from './homebrew.model.js';
import express from 'express';
import zlib from 'zlib';
import GoogleActions from './googleActions.js';
-import Markdown from '../shared/markdown.js';
+import { hbfm } from 'marked-hbfm';
import * as yaml from 'js-yaml';
import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid';
@@ -21,6 +21,8 @@ const router = express.Router();
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
import Themes from '../themes/themes.json' with { type: 'json' };
+import Stream from './eventStreamSource.js';
+
const isStaticTheme = (renderer, themeName)=>{
return Themes[renderer]?.[themeName] !== undefined;
};
@@ -168,8 +170,7 @@ const api = {
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
.catch((googleError)=>{
- const reason = googleError.errors?.[0].reason;
- if(reason == 'notFound')
+ if(googleError.code === 404 || googleError.status === 404)
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
else
throw { ...googleError, HBErrorCode: '01' };
@@ -233,7 +234,7 @@ const api = {
return text;
},
getGoodBrewTitle : (text)=>{
- const tokens = Markdown.marked.lexer(text);
+ const tokens = hbfm.marked.lexer(text);
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
.slice(0, MAX_TITLE_LENGTH);
},
@@ -501,6 +502,8 @@ const api = {
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
+ Stream.emit('sendUpdate', 'brewUpdated', { time: new Date, shareId: brew.shareId, version: brew.version });
+
res.status(200).send(saved);
},
deleteGoogleBrew : async (account, id, editId, res)=>{
@@ -574,9 +577,9 @@ const api = {
router.use(dbCheck);
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
-router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
+router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); //alt endpoint, unused
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
-router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
+router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); //alt endpoint, unused
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
diff --git a/server/page-routes.js b/server/page-routes.js
new file mode 100644
index 000000000..e2e7be193
--- /dev/null
+++ b/server/page-routes.js
@@ -0,0 +1,380 @@
+/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
+// page-routes.js
+
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+const __dirname = dirname(fileURLToPath(import.meta.url));
+process.chdir(`${__dirname}/..`);
+
+import _ from 'lodash';
+import express from 'express';
+import asyncHandler from 'express-async-handler';
+import fs from 'fs';
+
+//==== Middleware Imports ====//
+import dbCheck from './middleware/dbCheck.js';
+import sanitizeFilename from 'sanitize-filename';
+import { DEFAULT_BREW } from './brewDefaults.js';
+import { splitTextStyleAndMetadata } from '../shared/helpers.js';
+import GoogleActions from './googleActions.js';
+
+import api from './homebrew.api.js';
+const { getBrew, getUsersBrewThemes } = api;
+
+const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
+const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
+const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8');
+const changelogText = fs.readFileSync('changelog.md', 'utf8');
+const faqText = fs.readFileSync('faq.md', 'utf8');
+
+export default function pageRoutes({
+ defaultMetaTags,
+ HomebrewModel,
+ sanitizeBrew,
+}) {
+ const app = express.Router();
+
+ //Home page
+ app.get('/', (req, res, next)=>{
+ req.brew = {
+ text : welcomeText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Homepage',
+ description : 'Homepage'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Home page Legacy
+ app.get('/legacy', (req, res, next)=>{
+ req.brew = {
+ text : welcomeTextLegacy,
+ renderer : 'legacy',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Homepage (Legacy)',
+ description : 'Homepage'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Legacy/Other Document -> v3 Migration Guide
+ app.get('/migrate', (req, res, next)=>{
+ req.brew = {
+ text : migrateText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'v3 Migration Guide',
+ description : 'A brief guide to converting Legacy documents to the v3 renderer.'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Changelog page
+ app.get('/changelog', async (req, res, next)=>{
+ req.brew = {
+ title : 'Changelog',
+ text : changelogText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'Changelog',
+ description : 'Development changelog.'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //FAQ page
+ app.get('/faq', async (req, res, next)=>{
+ req.brew = {
+ title : 'FAQ',
+ text : faqText,
+ renderer : 'V3',
+ theme : '5ePHB'
+ },
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'FAQ',
+ description : 'Frequently Asked Questions'
+ };
+
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ });
+
+ //Source page
+ app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+
+ const replaceStrings = { '&': '&', '<': '<', '>': '>' };
+ let text = brew.text;
+ for (const replaceStr in replaceStrings) {
+ text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
+ }
+ text = `${text}`;
+ res.status(200).send(text);
+ });
+
+ //Download brew source page
+ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
+ const { brew } = req;
+ sanitizeBrew(brew, 'share');
+ const prefix = 'HB - ';
+
+ const encodeRFC3986ValueChars = (str)=>{
+ return (
+ encodeURIComponent(str)
+ .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
+ );
+ };
+
+ let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
+ if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
+ res.set({
+ 'Cache-Control' : 'no-cache',
+ 'Content-Type' : 'text/plain',
+ 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
+ });
+ res.status(200).send(brew.text);
+ });
+
+ //User Page
+ app.get('/user/:username', dbCheck, async (req, res, next)=>{
+ const ownAccount = req.account && (req.account.username == req.params.username);
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : `${req.params.username}'s Collection`,
+ description : 'View my collection of homebrew on the Homebrewery.'
+ // type : could be 'profile'?
+ };
+
+ const fields = [
+ 'googleId',
+ 'title',
+ 'pageCount',
+ 'description',
+ 'authors',
+ 'lang',
+ 'published',
+ 'views',
+ 'shareId',
+ 'editId',
+ 'createdAt',
+ 'updatedAt',
+ 'lastViewed',
+ 'thumbnail',
+ 'tags'
+ ];
+
+ let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
+ .catch((err)=>{
+ console.log(err);
+ });
+
+ brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
+
+ if(ownAccount && req?.account?.googleId){
+ const auth = await GoogleActions.authCheck(req.account, res);
+ let googleBrews = await GoogleActions.listGoogleBrews(auth)
+ .catch((err)=>{
+ console.error(err);
+ });
+
+ // If stub matches file from Google, use Google metadata over stub metadata
+ if(googleBrews && googleBrews.length > 0) {
+ for (const brew of brews.filter((brew)=>brew.googleId)) {
+ const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
+ if(match !== -1) {
+ brew.googleId = googleBrews[match].googleId;
+ brew.pageCount = googleBrews[match].pageCount;
+ brew.renderer = googleBrews[match].renderer;
+ brew.version = googleBrews[match].version;
+ brew.webViewLink = googleBrews[match].webViewLink;
+ googleBrews.splice(match, 1);
+ }
+ }
+
+ //Remaining unstubbed google brews display current user as author
+ googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
+ brews = _.concat(brews, googleBrews);
+ }
+ }
+
+ req.brews = _.map(brews, (brew)=>{
+ // Clean up brew data
+ brew.title = brew.title?.trim();
+ brew.description = brew.description?.trim();
+ return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
+ });
+
+ return next();
+ });
+
+ //Edit Page
+ 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.',
+ image : req.brew.thumbnail || defaultMetaTags.image,
+ locale : req.brew.lang,
+ type : 'article'
+ };
+
+ sanitizeBrew(req.brew, 'edit');
+ 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 from ID
+ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
+ sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ const brew = {
+ shareId : req.brew.shareId,
+ title : `CLONE - ${req.brew.title}`,
+ text : req.brew.text,
+ style : req.brew.style,
+ renderer : req.brew.renderer,
+ theme : req.brew.theme,
+ tags : req.brew.tags,
+ snippets : req.brew.snippets
+ };
+ 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', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
+ const { brew } = req;
+ req.ogMeta = { ...defaultMetaTags,
+ title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
+ description : req.brew.description || 'No description.',
+ image : req.brew.thumbnail || defaultMetaTags.image,
+ type : 'article'
+ };
+
+ // increase visitor view count, do not include visits by author(s)
+ if(!brew.authors.includes(req.account?.username)){
+ if(req.params.id.length > 12 && !brew._id) {
+ const googleId = brew.googleId;
+ const shareId = brew.shareId;
+ await GoogleActions.increaseView(googleId, shareId, 'share', brew)
+ .catch((err)=>{next(err);});
+ } else {
+ await HomebrewModel.increaseView({ shareId: brew.shareId });
+ }
+ };
+
+ brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
+ splitTextStyleAndMetadata(req.brew);
+ return next();
+ }));
+
+ //Account Page
+ app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
+ const data = {};
+ data.title = 'Account Information Page';
+
+ if(!req.account) {
+ res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
+ const error = new Error('No valid account');
+ error.status = 401;
+ error.HBErrorCode = '50';
+ error.page = data.title;
+ return next(error);
+ };
+
+ let auth;
+ let googleCount = [];
+ if(req.account) {
+ if(req.account.googleId) {
+ auth = await GoogleActions.authCheck(req.account, res, false);
+
+ googleCount = await GoogleActions.listGoogleBrews(auth)
+ .catch((err)=>{
+ console.error(err);
+ });
+ }
+
+ const query = { authors: req.account.username, googleId: { $exists: false } };
+ const mongoCount = await HomebrewModel.countDocuments(query)
+ .catch((err)=>{
+ console.log(err);
+ return 0;
+ });
+
+ data.accountDetails = {
+ username : req.account.username,
+ issued : req.account.issued,
+ googleId : Boolean(req.account.googleId),
+ authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
+ mongoCount : mongoCount,
+ googleCount : googleCount?.length
+ };
+ }
+
+ req.brew = data;
+
+ req.ogMeta = { ...defaultMetaTags,
+ title : `Account Page`,
+ description : null
+ };
+
+ return next();
+ }));
+
+ //Vault Page
+ app.get('/vault', asyncHandler(async(req, res, next)=>{
+ req.ogMeta = { ...defaultMetaTags,
+ title : 'The Vault',
+ description : 'Search for Brews'
+ };
+ return next();
+ }));
+
+ return app;
+}
\ No newline at end of file
diff --git a/shared/helpers.js b/shared/helpers.js
index 667d7b6b1..d20687843 100644
--- a/shared/helpers.js
+++ b/shared/helpers.js
@@ -146,7 +146,7 @@ const printCurrentBrew = async ()=>{
}
};
-const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
+const fetchThemeBundle = async (setError = ()=>{}, setThemeBundle = ()=>{}, renderer, theme)=>{
if(!renderer || !theme) return;
const res = await request
.get(`/api/theme/${renderer}/${theme}`)
@@ -229,5 +229,6 @@ export {
printCurrentBrew,
fetchThemeBundle,
brewSnippetsToJSON,
- debugTextMismatch
+ debugTextMismatch,
+ yamlSnippetsToText
};
diff --git a/shared/markdown.js b/shared/markdown.js
deleted file mode 100644
index c951838a6..000000000
--- a/shared/markdown.js
+++ /dev/null
@@ -1,574 +0,0 @@
-
-/* eslint-disable max-lines */
-import _ from 'lodash';
-import { marked as Marked } from 'marked';
-import MarkedExtendedTables from 'marked-extended-tables';
-import MarkedDefinitionLists from 'marked-definition-lists';
-import MarkedAlignedParagraphs from 'marked-alignment-paragraphs';
-import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces';
-import MarkedSubSuperText from 'marked-subsuper-text';
-import { markedVariables,
- setMarkedVariablePage,
- setMarkedVariable,
- getMarkedVariable } from 'marked-variables';
-import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
-import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
-import { markedEmoji as MarkedEmojis } from 'marked-emoji';
-import MarkedDiagramsMarkdeep from 'marked-diagrams-markdeep';
-
-
-//Icon fonts included so they can appear in emoji autosuggest dropdown
-import diceFont from '../themes/fonts/iconFonts/diceFont.js';
-import elderberryInn from '../themes/fonts/iconFonts/elderberryInn.js';
-import gameIcons from '../themes/fonts/iconFonts/gameIcons.js';
-import fontAwesome from '../themes/fonts/iconFonts/fontAwesome.js';
-
-const renderer = new Marked.Renderer();
-const tokenizer = new Marked.Tokenizer();
-
-//Processes the markdown within an HTML block if it's just a class-wrapper
-renderer.html = function (token) {
- let html = token.text;
- if(_.startsWith(_.trim(html), 'tags -renderer.paragraph = function(token){ - let match; - const text = this.parser.parseInline(token.tokens); - if(text.startsWith('
Bold text
\nParagraph
'); }); 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).trim(); + const rendered = hbfm.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('Term 1
\n\n`); }); test('Multiline Definition List must have at least one non-newline character after ::', function() { const source = 'Term 1\n::\nDefinition 1\n\n'; - const rendered = Markdown.render(source).trim(); + const rendered = hbfm.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n\n\nDefinition 1
`); }); }); diff --git a/tests/markdown/emojis.test.js b/tests/markdown/emojis.test.js index 072de10f9..129aaaa5e 100644 --- a/tests/markdown/emojis.test.js +++ b/tests/markdown/emojis.test.js @@ -1,4 +1,4 @@ -import Markdown from '../../shared/markdown.js'; +import { hbfm } from 'marked-hbfm'; import dedent from 'dedent'; // Marked.js adds line returns after closing tags on some default tokens. @@ -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).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); }); 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).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
:invalid:
`); }); it('two valid emojis with no whitespace are prioritized over definition lists', function() { const source = `:${emoji}::${emoji}:`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); }); 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).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`); }); it('Emojis are not parsed inside of curly span CSS blocks', function() { const source = `{{color:${emoji} text}}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text`); }); @@ -50,7 +50,7 @@ describe(`When emojis/icons are active`, ()=>{ const source = dedent`{{color:${emoji} text }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
text
Line 1
\n\n\nLine 2
`); }); test('Ignored inside a code block', function() { const source = '```\n\n:\n\n```\n'; - const rendered = Markdown.render(source).trim(); + const rendered = hbfm.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`\n:\n`);
});
});
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index 378263e58..d69691ef2 100644
--- a/tests/markdown/mustache-syntax.test.js
+++ b/tests/markdown/mustache-syntax.test.js
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import dedent from 'dedent';
-import Markdown from '../../shared/markdown.js';
+import { hbfm } from 'marked-hbfm';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
@@ -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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text only, but with spaces', function() {
const source = '{{ this is a text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('this is a text');
});
it('Renders an empty mustache span', function() {
const source = '{{}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
it('Renders a mustache span with just a space', function() {
const source = '{{ }}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
it('Renders a mustache span with a few spaces only', function() {
const source = '{{ }}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('');
});
it('Renders a mustache span with text and class', function() {
const source = '{{my-class text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two classes', function() {
const source = '{{my-class,my-class2 text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('this is a text');
});
it('Renders a mustache span with text and id', function() {
const source = '{{#my-span text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two ids', function() {
const source = '{{#my-span,#my-favorite-span text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and css property', function() {
const source = '{{color:red text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
it('Renders a mustache span with text and two css properties', function() {
const source = '{{color:red,padding:5px text}}';
- const rendered = Markdown.render(source);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text “with quotes”');
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text “with quotes”`);
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text');
});
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);
+ const rendered = hbfm.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('Text and text and more text!
\n'); }); }); @@ -132,7 +132,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ const source = dedent`{{ text }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text
{{}}
`); }); @@ -155,7 +155,7 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ const source = dedent`{{cat }}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); }); @@ -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).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Sample text.
Text and text and more text!
\nemphasis
'); }); it('Renders a code element with injected style', function() { const source = '`code`{background:gray}'; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('code

text{background:blue}
'); }); 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 = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('bolded text
'); }); 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).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`


text
text
text
text
text
text
text
text
text
text
| Experience Points | Level |
|---|---|
| 0 | 1 |
| 300 | 2 |
{secondInjection}
'); }); @@ -487,7 +487,7 @@ describe('Injection: When an injection tag follows an element', ()=>{ {innerDiv} }} {outerDiv}`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('outer text
inner text
Hello
`); }); test('Right Justify', function() { const source = '-: Hello'; - const rendered = Markdown.render(source); + const rendered = hbfm.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Hello
`); }); test('Center Justify', function() { const source = ':-: Hello'; - const rendered = Markdown.render(source); + const rendered = hbfm.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Hello
`); }); test('Ignored inside a code block', function() { const source = '```\n\n:- Hello\n\n```\n'; - const rendered = Markdown.render(source); + const rendered = hbfm.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`\n:- Hello\n\n`);
});
});
diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js
index 180839a78..5dc18592a 100644
--- a/tests/markdown/variables.test.js
+++ b/tests/markdown/variables.test.js
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import dedent from 'dedent';
-import Markdown from '../../shared/markdown.js';
+import { hbfm } from 'marked-hbfm';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
@@ -12,7 +12,7 @@ String.prototype.trimReturns = function(){
const renderAllPages = function(pages){
const outputs = [];
pages.forEach((page, index)=>{
- const output = Markdown.render(page, index);
+ const output = hbfm.render(page, index);
outputs.push(output);
});
@@ -29,7 +29,7 @@ describe('Block-level variables', ()=>{
$[var]
`;
- const rendered = Markdown.render(source).trimReturns();
+ const rendered = hbfm.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('string
'); }); @@ -40,7 +40,7 @@ describe('Block-level variables', ()=>{ lines $[var]`; - const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + const rendered = hbfm.render(source).replace(/\s/g, ' ').trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('string across multiple lines
'); }); @@ -54,7 +54,7 @@ describe('Block-level variables', ()=>{ | C | D | $[var]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`| H1 | @@ -71,7 +71,7 @@ describe('Block-level variables', ()=>{ $[var] [var]: string`; - const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns(); + const rendered = hbfm.render(source).replace(/\s/g, ' ').trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
|---|
| title 1 | title 2 | title 3 | title 4 |
|---|---|---|---|
| foo | Ipsum | ) | ) |




Positive: +
Negative: -
'); }); it('Signed Test', function() { const source = `[a]: 13\n\n[b]: -11\n\nPositive: $[signed(a)]\n\nNegative: $[signed(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Positive: +13
Negative: -11
'); }); it('Roman Numerals Test', function() { const source = `[a]: 18\n\nRoman Numeral: $[toRomans(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Roman Numeral: XVIII
'); }); it('Roman Numerals Test - Uppercase', function() { const source = `[a]: 18\n\nRoman Numeral: $[toRomansUpper(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Roman Numeral: XVIII
'); }); it('Roman Numerals Test - Lowercase', function() { const source = `[a]: 18\n\nRoman Numeral: $[toRomansLower(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Roman Numeral: xviii
'); }); it('Number to Characters Test', function() { const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toChar(a)] $[toChar(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Characters: R AM
'); }); it('Number to Characters Test - Uppercase', function() { const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toCharUpper(a)] $[toCharUpper(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Characters: R AM
'); }); it('Number to Characters Test - Lowercase', function() { const source = `[a]: 18\n\n[b]: 39\n\nCharacters: $[toCharLower(a)] $[toCharLower(b)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Characters: r am
'); }); it('Number to Words Test', function() { const source = `[a]: 80085\n\nWords: $[toWords(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: eighty thousand and eighty-five
'); }); it('Number to Words Test - Uppercase', function() { const source = `[a]: 80085\n\nWords: $[toWordsUpper(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: EIGHTY THOUSAND AND EIGHTY-FIVE
'); }); it('Number to Words Test - Lowercase', function() { const source = `[a]: 80085\n\nWords: $[toWordsLower(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: eighty thousand and eighty-five
'); }); it('Number to Words Test - Capitalized', function() { const source = `[a]: 80085\n\nWords: $[toWordsCaps(a)]`; - const rendered = Markdown.render(source).trimReturns(); + const rendered = hbfm.render(source).trimReturns(); expect(rendered).toBe('Words: Eighty Thousand And Eighty-Five
'); }); }); \ No newline at end of file diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index d79533c2c..b0452d882 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,6 +14,8 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ + p.first-letter::first-letter, + p.drop-cap::first-letter, h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index fee12e1f6..fd2a3758c 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,6 +30,8 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ + .page p.first-letter::first-letter, + .page p.drop-cap::first-letter, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 12601bb5d..0dd333e9e 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,23 +85,25 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } & + p::first-line { font-variant : small-caps; } } + p.first-letter::first-letter, + p.drop-cap::first-letter, + h1 + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; @@ -196,7 +198,7 @@ } & + * { margin-top : 0.54cm; } - + } // ***************************** @@ -279,7 +281,7 @@ .watermark { color : black; } /* Watercolor */ - + .watercolor1 { --wc : @watercolor1; } .watercolor2 { --wc : @watercolor2; } .watercolor3 { --wc : @watercolor3; } diff --git a/themes/V3/Blank/snippets/footer.gen.js b/themes/V3/Blank/snippets/footer.gen.js index 596592629..9b5295f69 100644 --- a/themes/V3/Blank/snippets/footer.gen.js +++ b/themes/V3/Blank/snippets/footer.gen.js @@ -1,4 +1,4 @@ -import Markdown from '@shared/markdown.js'; +import { hbfm } from 'marked-hbfm'; export default { createFooterFunc : function(headerSize=1){ diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 74c976f47..8d6f3f9ff 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,21 +80,23 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; - & + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } & + p::first-line { font-variant : small-caps; } } + p.first-letter::first-letter, + p.drop-cap::first-letter, + h1 + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly