0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-05-07 18:48:39 +00:00

Initial Commit

Adds menu items for "regular", zipped, and inline output.

Currently only displays inline output with *no* URL massaging ( all relative path references are still relative )

Displays, not downloads
This commit is contained in:
David Bolack
2026-01-09 16:22:07 -06:00
parent 0495513059
commit 5faa12a79e
8 changed files with 169 additions and 20 deletions
+9
View File
@@ -0,0 +1,9 @@
const React = require('react');
const Nav = require('client/homebrew/navbar/nav.jsx');
const { printCurrentBrew } = require('../../../shared/helpers.js');
module.exports = function(props){
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF
</Nav.item>;
};
+18 -4
View File
@@ -2,8 +2,22 @@ const React = require('react');
const Nav = require('client/homebrew/navbar/nav.jsx'); const Nav = require('client/homebrew/navbar/nav.jsx');
const { printCurrentBrew } = require('../../../shared/helpers.js'); const { printCurrentBrew } = require('../../../shared/helpers.js');
module.exports = function(){ module.exports = function(props){
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'> return <Nav.dropdown>
get PDF <Nav.item color='grey' icon='fas fa-question-circle'>
</Nav.item>; export
</Nav.item>
<Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF
</Nav.item>
<Nav.item color='orange' icon='fas fa-file-code' href={`/export/slimHTML/${props?.brew?.editId || props?.brew?.shareId}`}>
get HTML (Slim)
</Nav.item>
<Nav.item color='orange' icon='fas fa-file-archive' href={`/export/zipHTML/${props?.brew?.editId || props?.brew?.shareId}`}>
get HTML (Zip)
</Nav.item>
<Nav.item color='orange' icon='far fa-file-code' href={`/export/inlineHTML/${props?.brew?.editId || props?.brew?.shareId}`}>
get HTML (Inline)
</Nav.item>
</Nav.dropdown>;
}; };
+1 -1
View File
@@ -360,7 +360,7 @@ const EditPage = (props)=>{
{renderAutoSaveButton()} {renderAutoSaveButton()}
</Nav.dropdown>} </Nav.dropdown>}
<NewBrewItem /> <NewBrewItem />
<PrintNavItem /> <PrintNavItem brew={currentBrew}/>
<HelpNavItem /> <HelpNavItem />
<VaultNavItem /> <VaultNavItem />
<ShareNavItem brew={currentBrew} /> <ShareNavItem brew={currentBrew} />
+1 -1
View File
@@ -179,7 +179,7 @@ const HomePage =(props)=>{
? <ErrorNavItem error={error} clearError={clearError} /> ? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()} : renderSaveButton()}
<NewBrewItem /> <NewBrewItem />
<PrintNavItem /> <PrintNavItem brew={currentBrew}/>
<HelpNavItem /> <HelpNavItem />
<VaultNavItem /> <VaultNavItem />
<RecentNavItem /> <RecentNavItem />
+2 -2
View File
@@ -21,7 +21,7 @@ import AccountNavItem from 'client/homebrew/navbar/account.navitem.js
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx'; import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx'; import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx'; import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx'; import PDFNavItem from 'client/homebrew/navbar/pdf.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx'; import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx';
// Page specific imports // Page specific imports
@@ -229,7 +229,7 @@ const NewPage = (props)=>{
? <ErrorNavItem error={error} clearError={clearError} /> ? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()} : renderSaveButton()}
<NewBrewItem /> <NewBrewItem />
<PrintNavItem /> <PDFNavItem />
<HelpNavItem /> <HelpNavItem />
<VaultNavItem /> <VaultNavItem />
<RecentNavItem /> <RecentNavItem />
@@ -76,7 +76,7 @@ const SharePage = (props)=>{
<Nav.section> <Nav.section>
{brew.shareId && ( {brew.shareId && (
<> <>
<PrintNavItem /> <PrintNavItem brew={currentBrew}/>
<Nav.dropdown> <Nav.dropdown>
<Nav.item color='red' icon='fas fa-code'> <Nav.item color='red' icon='fas fa-code'>
source source
+32 -10
View File
@@ -25,10 +25,11 @@ import serveCompressedStaticAssets from './static-assets.mv.js';
import sanitizeFilename from 'sanitize-filename'; import sanitizeFilename from 'sanitize-filename';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import templateFn from '../client/template.js'; import templateFn from '../client/template.js';
import { model as HomebrewModel } from './homebrew.model.js'; import { model as HomebrewModel } from './homebrew.model.js';
import { DEFAULT_BREW } from './brewDefaults.js'; import { DEFAULT_BREW } from './brewDefaults.js';
import { splitTextStyleAndMetadata } from '../shared/helpers.js'; import { splitTextStyleAndMetadata,
simulateRender } from '../shared/helpers.js';
//==== Middleware Imports ====// //==== Middleware Imports ====//
import contentNegotiation from './middleware/content-negotiation.js'; import contentNegotiation from './middleware/content-negotiation.js';
@@ -47,6 +48,14 @@ const sanitizeBrew = (brew, accessType)=>{
return brew; return brew;
}; };
const encodeRFC3986ValueChars = (str)=>{
return (
encodeURIComponent(str)
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
);
};
app.set('trust proxy', 1 /* number of proxies between user and server */); app.set('trust proxy', 1 /* number of proxies between user and server */);
app.use('/', serveCompressedStaticAssets(`build`)); app.use('/', serveCompressedStaticAssets(`build`));
@@ -231,19 +240,32 @@ app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
res.status(200).send(text); res.status(200).send(text);
}); });
//Export the Brew as HTML
app.get('/export/:mode/:id', asyncHandler(getBrew('admin')), asyncHandler(simulateRender), (req, res)=>{
const id = req.params.id;
const mode = req.params.mode;
const { brew } = req;
sanitizeBrew(brew, 'share');
const prefix = 'HB - ';
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)}.html`
// });
res.status(200).send(brew.html);
});
//Download brew source page //Download brew source page
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
const { brew } = req; const { brew } = req;
sanitizeBrew(brew, 'share'); sanitizeBrew(brew, 'share');
const prefix = 'HB - '; 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(' ', ''); let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({ res.set({
+105 -1
View File
@@ -1,6 +1,14 @@
/* eslint-disable max-lines */
import _ from 'lodash'; import _ from 'lodash';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import request from '../client/homebrew/utils/request-middleware.js'; import request from '../client/homebrew/utils/request-middleware.js';
import Markdown from '../shared/markdown.js';
import packageJSON from '../package.json' with { type: 'json' };
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
// Convert the templates from a brew to a Snippets Structure. // Convert the templates from a brew to a Snippets Structure.
const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=null, full=true)=>{ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=null, full=true)=>{
@@ -168,10 +176,106 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
} }
}; };
const simulateRenderPage = (pageText, index, renderer)=>{
let styles = {};
let classes = 'page';
let attributes = {};
if(renderer == 'legacy') {
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
// const html = MarkdownLegacy.render(pageText);
const html = "Markdown Legacy currently unsupported"
return `<div className='page phb' index=${index} key=${index} style=${styles}>\n${html}\n</div>\n`;
} else {
if(pageText.startsWith('\\page')) {
const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
if(injectedTags) {
styles = { ...styles, ...injectedTags.styles };
styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)).join(''); // Convert CSS to camelCase for React
classes = [classes, injectedTags.classes].join(' ').trim();
attributes = injectedTags.attributes;
}
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
}
// DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME.
pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
const html = Markdown.render(pageText, index);
return `<div class=${classes} id=p${index} key=${index} style=${styles} ${attributes}>\n${html}\n</div>`;
}
};
const simulateRender = async (req, res, next)=>{
let htmlHead = '';
let htmlStyles = '';
let htmlBody = '';
let errorMsg = {};
// Build HTML similar to the BrewRender ?
const setError = (error)=>{
errorMsg = error;
};
splitTextStyleAndMetadata(req.brew);
const PORT = req.header('host').indexOf[':'] > -1 ? req.header('host').split(':')[1] : '8000';
const themeRes = await request
.get(`http://localhost:${PORT}/api/theme/${req.brew.renderer}/${req.brew.theme}`)
.set('Homebrewery-Version', packageJSON.version)
.catch((err)=>{
setError(err);
});
const htmlThemeBundle = themeRes.body.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
// Create Head
htmlHead += ` <head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css">
<link href="/homebrew/bundle.css" type="text/css" rel="stylesheet">
<base target="_blank">
<title>${req.brew.title}</title>
</head>`;
htmlStyles = `\t<div style="display:none;">\n` +
`\t\t${htmlThemeBundle}\n` +
`\t\t<style>\n${req.brew.style}\n</style>\n` +
`\t</div>`;
let rawPages = [];
let renderedPages = [];
if(req.brew.renderer == 'legacy') {
rawPages = req.brew.text.split(PAGEBREAK_REGEX_LEGACY);
} else {
rawPages = req.brew.text.split(PAGEBREAK_REGEX_V3);
}
_.forEach(rawPages, (page, index)=>{
renderedPages[index] = simulateRenderPage(page, index, req.brew.renderer);
});
htmlBody = `<div class="pages recto single" lang="en" style="zoom: 100%; gap: 5px 10px;">${renderedPages.join('\n')}\n</div`;
const result = `<html>\n${htmlHead}\n<body>\n\t<div>\n\t\t<div class="frame-content">\n\t\t\t<div class="brewRenderer">\n` +
`\t\t\t\t${htmlStyles}\n${htmlBody}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</body>\n</html>`;
req.brew.html = result;
next();
};
export { export {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,
printCurrentBrew, printCurrentBrew,
fetchThemeBundle, fetchThemeBundle,
brewSnippetsToJSON, brewSnippetsToJSON,
debugTextMismatch debugTextMismatch,
simulateRender,
}; };