diff --git a/changelog.md b/changelog.md index 18c3205f7..0a9e509f2 100644 --- a/changelog.md +++ b/changelog.md @@ -84,6 +84,45 @@ pre { ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). +### Friday 28/6/2024 - v3.13.0 +{{taskList + +##### calculuschild + +* [x] Add `:emoji:` Markdown syntax, with autosuggest; start typing after the first `:` for matching emojis from +:fab_font_awesome: FontAwesome, :df_d20: DiceFont, :ei_action: ElderberryInn, and a subset of :gi_broadsword: GameIcons + +* [x] Fix `{curly injection}` to append to, rather than erase and replace target CSS +* [x] {{openSans **GET PDF**}} {{fa,fa-file-pdf}} now opens the print dialog directly, rather than redirecting to a separate page + +##### Gazook + +* [x] Several small style tweaks to the UI +* [x] Cleaning and refactoring several large pieces of code + +##### 5e-Cleric + +* [x] For error pages, add links to user account and `/share` page if available + +Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298) + +* [x] Change FrontCover title to use stroke outline instead of faking it with dozens of shadows +* [x] Cleaning and refactoring several large pieces of CSS + +##### abquintic + +* [x] Added additional {{openSans **TABLE OF CONTENTS**}} snippet options. Explicitly include or exclude items from the ToC generation via CSS properties +`--TOC:exclude` or `--TOC:include`, or change the included header depth from 3 to 6 (default 3) with `tocDepthH6` + +##### MurdoMaclachlan *(new contributor!)* + +* [x] Added "proficiency bonus" to Monster Stat Block snippet. + +Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397) +}} + +\column + ### Monday 18/3/2024 - v3.12.0 {{taskList diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 7eae3228f..69d0ab65c 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -37,7 +37,7 @@ const BrewPage = (props)=>{ index : 0, ...props }; - const cleanText = DOMPurify.sanitize(props.contents, purifyConfig); + const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig); return
; @@ -126,7 +126,7 @@ const BrewRenderer = (props)=>{ const renderStyle = ()=>{ if(!props.style) return; - const cleanStyle = DOMPurify.sanitize(props.style, purifyConfig); + const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig); //return
@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} ` }} />; return
${cleanStyle} ` }} />; }; diff --git a/package-lock.json b/package-lock.json index 1b55c40a0..9ccc49e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebrewery", - "version": "3.12.0", + "version": "3.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebrewery", - "version": "3.12.0", + "version": "3.13.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b5b7824b3..83e180280 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebrewery", "description": "Create authentic looking D&D homebrews using only markdown", - "version": "3.12.0", + "version": "3.13.0", "engines": { "npm": "^10.2.x", "node": "^20.8.x" diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index 0f29eff7b..cb73b0a88 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -8,6 +8,7 @@ @import (less) './themes/fonts/iconFonts/diceFont.less'; @import (less) './themes/fonts/iconFonts/elderberryInn.less'; @import (less) './themes/fonts/iconFonts/gameIcons.less'; +@import (less) './themes/fonts/iconFonts/fontAwesome.less'; @keyframes sourceMoveAnimation { 50% {background-color: red; color: white;} diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 95431487d..529129833 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -102,7 +102,7 @@ const mustacheSpans = { start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token - const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; + const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { //Find closing delimiter @@ -159,7 +159,7 @@ const mustacheDivs = { start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token - const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; + const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { //Find closing delimiter @@ -214,7 +214,7 @@ const mustacheInjectInline = { level : 'inline', start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -265,7 +265,7 @@ const mustacheInjectBlock = { level : 'block', start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -771,7 +771,8 @@ const processStyleTags = (string)=>{ const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')) ?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) .reduce((obj, attr)=>{ - let [key, value] = attr.split('='); + const index = attr.indexOf('='); + let [key, value] = [attr.substring(0, index), attr.substring(index + 1)]; value = value.replace(/"/g, ''); obj[key] = value; return obj; @@ -793,7 +794,8 @@ const extractHTMLStyleTags = (htmlString)=>{ const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g) ?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) .reduce((obj, attr)=>{ - let [key, value] = attr.split('='); + const index = attr.indexOf('='); + let [key, value] = [attr.substring(0, index), attr.substring(index + 1)]; value = value.replace(/"/g, ''); obj[key] = value; return obj; diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index b32876353..7b0115cae 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -338,6 +338,18 @@ describe('Injection: When an injection tag follows an element', ()=>{ const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

`); }); + + it('Renders an image with "=" in the url, and added attributes', function() { + const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png?auth=12345&height=1024) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

`); + }); + + it('Renders an image and added attributes with "=" in the value, ', function() { + const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

`); + }); }); describe('and that element is a block', ()=>{ diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index c0933d70d..d5f37ac65 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -21,9 +21,43 @@ module.exports = [ view : 'text', snippets : [ { - name : 'Table of Contents', - icon : 'fas fa-book', - gen : TableOfContentsGen + name : 'Table of Contents', + icon : 'fas fa-book', + gen : TableOfContentsGen, + experimental : true, + subsnippets : [ + { + name : 'Table of Contents', + icon : 'fas fa-book', + gen : TableOfContentsGen, + experimental : true + }, + { + name : 'Include in ToC up to H3', + icon : 'fas fa-dice-three', + gen : dedent `\n{{tocDepthH3 + }}\n`, + + }, + { + name : 'Include in ToC up to H4', + icon : 'fas fa-dice-four', + gen : dedent `\n{{tocDepthH4 + }}\n`, + }, + { + name : 'Include in ToC up to H5', + icon : 'fas fa-dice-five', + gen : dedent `\n{{tocDepthH5 + }}\n`, + }, + { + name : 'Include in ToC up to H6', + icon : 'fas fa-dice-six', + gen : dedent `\n{{tocDepthH6 + }}\n`, + } + ] }, { name : 'Index', diff --git a/themes/V3/5ePHB/snippets/tableOfContents.gen.js b/themes/V3/5ePHB/snippets/tableOfContents.gen.js index 04ff77f3f..b212dea36 100644 --- a/themes/V3/5ePHB/snippets/tableOfContents.gen.js +++ b/themes/V3/5ePHB/snippets/tableOfContents.gen.js @@ -2,77 +2,68 @@ const _ = require('lodash'); const dedent = require('dedent-tabs').default; const getTOC = (pages)=>{ - const add1 = (title, page)=>{ - res.push({ - title : title, - page : page + 1, - children : [] - }); - }; - const add2 = (title, page)=>{ - if(!_.last(res)) add1(null, page); - _.last(res).children.push({ - title : title, - page : page + 1, - children : [] - }); - }; - const add3 = (title, page)=>{ - if(!_.last(res)) add1(null, page); - if(!_.last(_.last(res).children)) add2(null, page); - _.last(_.last(res).children).children.push({ - title : title, - page : page + 1, - children : [] - }); + + const recursiveAdd = (title, page, targetDepth, child, curDepth=0)=>{ + if(curDepth > 5) return; // Something went wrong. + if(curDepth == targetDepth) { + child.push({ + title : title, + page : page, + children : [] + }); + } else { + if(child.length == 0) { + child.push({ + title : null, + page : page, + children : [] + }); + } + recursiveAdd(title, page, targetDepth, _.last(child).children, curDepth+1,); + } }; const res = []; - _.each(pages, (page, pageNum)=>{ - if(!page.includes('{{frontCover}}') && !page.includes('{{insideCover}}') && !page.includes('{{partCover}}') && !page.includes('{{backCover}}')) { - const lines = page.split('\n'); - _.each(lines, (line)=>{ - if(_.startsWith(line, '# ')){ - const title = line.replace('# ', ''); - add1(title, pageNum); - } - if(_.startsWith(line, '## ')){ - const title = line.replace('## ', ''); - add2(title, pageNum); - } - if(_.startsWith(line, '### ')){ - const title = line.replace('### ', ''); - add3(title, pageNum); - } - }); + + const iframe = document.getElementById('BrewRenderer'); + const iframeDocument = iframe.contentDocument || iframe.contentWindow.document; + const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6'); + const headerDepth = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']; + + _.each(headings, (heading)=>{ + const onPage = parseInt(heading.closest('.page').id?.replace(/^p/, '')); + const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC'); + + if(ToCExclude != 'exclude') { + recursiveAdd(heading.innerText.trim(), onPage, headerDepth.indexOf(heading.tagName), res); } }); return res; }; + +const ToCIterate = (entries, curDepth=0)=>{ + const levelPad = ['- ###', ' - ####', ' - ', ' - ', ' - ', ' - ']; + const toc = []; + if(entries.title !== null){ + toc.push(`${levelPad[curDepth]} [{{ ${entries.title}}}{{ ${entries.page}}}](#p${entries.page})`); + } + if(entries.children.length) { + _.each(entries.children, (entry, idx)=>{ + const children = ToCIterate(entry, entry.title == null ? curDepth : curDepth+1); + if(children.length) { + toc.push(...children); + } + }); + } + return toc; +}; + module.exports = function(props){ const pages = props.brew.text.split('\\page'); const TOC = getTOC(pages); const markdown = _.reduce(TOC, (r, g1, idx1)=>{ - if(g1.title !== null) { - r.push(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`); - } - if(g1.children.length){ - _.each(g1.children, (g2, idx2)=>{ - if(g2.title !== null) { - r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`); - } - if(g2.children.length){ - _.each(g2.children, (g3, idx3)=>{ - if(g2.title !== null) { - r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`); - } else { // Don't over-indent if no level-2 parent entry - r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`); - } - }); - } - }); - } + r.push(ToCIterate(g1).join('\n')); return r; }, []).join('\n'); diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 400174ab0..f8a14f46e 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -786,6 +786,39 @@ // ***************************** // * TABLE OF CONTENTS // *****************************/ + +// Default Exclusions +// Anything not exlcuded is included, default Headers are H1, H2, and H3. +h4, +h5, +h6, +.page:has(.frontCover), +.page:has(.backCover), +.page:has(.insideCover), +.monster, +.noToC, +.toc { --TOC: exclude; } + +.tocDepthH2 :is(h1, h2) {--TOC: include; } +.tocDepthH3 :is(h1, h2, h3) {--TOC: include; } +.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; } +.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; } +.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; } + +.tocIncludeH1 h1 {--TOC: include; } +.tocIncludeH2 h2 {--TOC: include; } +.tocIncludeH3 h3 {--TOC: include; } +.tocIncludeH4 h4 {--TOC: include; } +.tocIncludeH5 h5 {--TOC: include; } +.tocIncludeH6 h6 {--TOC: include; } + +.page:has(.partCover) { + --TOC: exclude; + & h1 { + --TOC: include; + } + } + .page { &:has(.toc)::after { display : none; } .toc { diff --git a/themes/V3/Blank/style.less b/themes/V3/Blank/style.less index 24e87504f..0f779c38b 100644 --- a/themes/V3/Blank/style.less +++ b/themes/V3/Blank/style.less @@ -3,6 +3,7 @@ @import (less) './themes/fonts/iconFonts/elderberryInn.less'; @import (less) './themes/fonts/iconFonts/diceFont.less'; @import (less) './themes/fonts/iconFonts/gameIcons.less'; +@import (less) './themes/fonts/iconFonts/fontAwesome.less'; :root { //Colors diff --git a/themes/fonts/iconFonts/diceFont.less b/themes/fonts/iconFonts/diceFont.less index 6fe226a05..ec80f132b 100644 --- a/themes/fonts/iconFonts/diceFont.less +++ b/themes/fonts/iconFonts/diceFont.less @@ -7,7 +7,7 @@ } .df { - display : inline-block; + display : inline; font-family : 'DiceFont'; font-style : normal; font-weight : normal; @@ -16,8 +16,11 @@ text-decoration : inherit; text-transform : none; text-rendering : optimizeLegibility; - -moz-osx-font-smoothing : grayscale; + + /* Better Font Rendering =========== */ -webkit-font-smoothing : antialiased; + -moz-osx-font-smoothing : grayscale; + &.F::before { content : '\f190'; } &.F-minus::before { content : '\f191'; } &.F-plus::before { content : '\f192'; } diff --git a/themes/fonts/iconFonts/elderberryInn.less b/themes/fonts/iconFonts/elderberryInn.less index c956563fc..958d1b265 100644 --- a/themes/fonts/iconFonts/elderberryInn.less +++ b/themes/fonts/iconFonts/elderberryInn.less @@ -7,15 +7,16 @@ } .ei { - display : inline-block; - margin-right : 3px; + display : inline; font-family : 'Elderberry-Inn'; line-height : 1; vertical-align : baseline; - -moz-osx-font-smoothing : grayscale; - -webkit-font-smoothing : antialiased; text-rendering : auto; + /* Better Font Rendering =========== */ + -webkit-font-smoothing : antialiased; + -moz-osx-font-smoothing : grayscale; + &.book::before { content : '\E900'; } &.screen::before { content : '\E901'; } diff --git a/themes/fonts/iconFonts/fontAwesome.less b/themes/fonts/iconFonts/fontAwesome.less new file mode 100644 index 000000000..5f626c645 --- /dev/null +++ b/themes/fonts/iconFonts/fontAwesome.less @@ -0,0 +1,2 @@ +/* Icon Font: Font Awesome */ +.far,.fas,.fab { display : inline; } \ No newline at end of file diff --git a/themes/fonts/iconFonts/gameIcons.less b/themes/fonts/iconFonts/gameIcons.less index ea7b3aba5..a32ebdd08 100644 --- a/themes/fonts/iconFonts/gameIcons.less +++ b/themes/fonts/iconFonts/gameIcons.less @@ -8,19 +8,15 @@ .gi { /* use !important to prevent issues with browser extensions that change fonts */ - display : inline-block; - margin-right : 3px; + display : inline; font-family : 'Game-Icons' !important; line-height : 1; vertical-align : baseline; - -moz-osx-font-smoothing : grayscale; - -webkit-font-smoothing : antialiased; text-rendering : auto; /* Better Font Rendering =========== */ -webkit-font-smoothing : antialiased; -moz-osx-font-smoothing : grayscale; - &.zigzag-leaf::before { content : '\e900'; } &.zebra-shield::before { content : '\e901'; }