diff --git a/client/homebrew/navbar/pdf.navitem.jsx b/client/homebrew/navbar/pdf.navitem.jsx new file mode 100644 index 000000000..4ab0e0191 --- /dev/null +++ b/client/homebrew/navbar/pdf.navitem.jsx @@ -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 + get PDF + ; +}; diff --git a/client/homebrew/navbar/print.navitem.jsx b/client/homebrew/navbar/print.navitem.jsx index ccad820fa..73687592b 100644 --- a/client/homebrew/navbar/print.navitem.jsx +++ b/client/homebrew/navbar/print.navitem.jsx @@ -2,8 +2,22 @@ const React = require('react'); const Nav = require('client/homebrew/navbar/nav.jsx'); const { printCurrentBrew } = require('../../../shared/helpers.js'); -module.exports = function(){ - return - get PDF - ; +module.exports = function(props){ + return + + export + + + get PDF + + + get HTML (Slim) + + + get HTML (Zip) + + + get HTML (Inline) + + ; }; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 595436c5b..584daac3a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -360,7 +360,7 @@ const EditPage = (props)=>{ {renderAutoSaveButton()} } - + diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 463df333b..943a8f76a 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -179,7 +179,7 @@ const HomePage =(props)=>{ ? : renderSaveButton()} - + diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 83eaeda45..b3c7115d1 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -21,7 +21,7 @@ import AccountNavItem from 'client/homebrew/navbar/account.navitem.js import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx'; import HelpNavItem from 'client/homebrew/navbar/help.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'; // Page specific imports @@ -229,7 +229,7 @@ const NewPage = (props)=>{ ? : renderSaveButton()} - + diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 32e88c9d8..9c654943c 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -76,7 +76,7 @@ const SharePage = (props)=>{ {brew.shareId && ( <> - + source diff --git a/server/app.js b/server/app.js index 1bdb5aac3..1be4cbe77 100644 --- a/server/app.js +++ b/server/app.js @@ -25,10 +25,11 @@ import serveCompressedStaticAssets from './static-assets.mv.js'; import sanitizeFilename from 'sanitize-filename'; import asyncHandler from 'express-async-handler'; 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 { splitTextStyleAndMetadata } from '../shared/helpers.js'; +import { DEFAULT_BREW } from './brewDefaults.js'; +import { splitTextStyleAndMetadata, + simulateRender } from '../shared/helpers.js'; //==== Middleware Imports ====// import contentNegotiation from './middleware/content-negotiation.js'; @@ -47,6 +48,14 @@ const sanitizeBrew = (brew, accessType)=>{ 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.use('/', serveCompressedStaticAssets(`build`)); @@ -231,19 +240,32 @@ app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{ 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 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({ diff --git a/shared/helpers.js b/shared/helpers.js index adf5b889a..8936235d1 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -1,6 +1,14 @@ +/* eslint-disable max-lines */ import _ from 'lodash'; import yaml from 'js-yaml'; 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. 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 `\n${html}\n\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 \n\\column\n `; //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 `\n${html}\n`; + } +}; + + +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)=>``).join('\n\n'); + + // Create Head + htmlHead += ` + + + + ${req.brew.title} + `; + + htmlStyles = `\t\n` + + `\t\t${htmlThemeBundle}\n` + + `\t\t\n` + + `\t`; + + 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 = `${renderedPages.join('\n')}\n\n${htmlHead}\n\n\t\n\t\t\n\t\t\t\n` + + `\t\t\t\t${htmlStyles}\n${htmlBody}\n\t\t\t\n\t\t\n\t\n\n