diff --git a/package-lock.json b/package-lock.json index eccc537ff..2e3af673a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,9 +48,11 @@ "react-dom": "^18.3.1", "react-frame-component": "^4.1.3", "react-router": "^7.4.0", + "romans": "^3.0.0", "sanitize-filename": "1.6.3", "superagent": "^10.2.0", - "vitreum": "git+https://git@github.com/calculuschild/vitreum.git" + "vitreum": "git+https://git@github.com/calculuschild/vitreum.git", + "written-number": "^0.11.1" }, "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.2", @@ -12120,6 +12122,12 @@ "inherits": "^2.0.1" } }, + "node_modules/romans": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/romans/-/romans-3.0.0.tgz", + "integrity": "sha512-7DDsAfhtpRr/ZFQXiHDrC3Pe00agcAsFiNt5nNx4ZAQlsc6yJG0mvXA5WAvO8YZyOg349twm2GYhHLw7rCXAzw==", + "license": "MIT" + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -14904,6 +14912,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/written-number": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/written-number/-/written-number-0.11.1.tgz", + "integrity": "sha512-LhQ68uUnzHH0bwm/QiGA9JwqgadSDOwqB2AIs/LBsrOY6ScqVXKRN2slTCeKAhstDBJ/Of/Yxcjn0pnQmVlmtg==", + "license": "MIT" + }, "node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", diff --git a/package.json b/package.json index 94ac51006..8e88a3fd4 100644 --- a/package.json +++ b/package.json @@ -122,9 +122,11 @@ "react-dom": "^18.3.1", "react-frame-component": "^4.1.3", "react-router": "^7.4.0", + "romans": "^3.0.0", "sanitize-filename": "1.6.3", "superagent": "^10.2.0", - "vitreum": "git+https://git@github.com/calculuschild/vitreum.git" + "vitreum": "git+https://git@github.com/calculuschild/vitreum.git", + "written-number": "^0.11.1" }, "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.2", diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 485aa281f..6391b511b 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -8,6 +8,8 @@ import { markedSmartypantsLite as MarkedSmartypantsLite } import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id'; import { markedEmoji as MarkedEmojis } from 'marked-emoji'; import MarkedSubSuperText from 'marked-subsuper-text'; +import { romanize } from 'romans'; +import writtenNumber from 'written-number'; //Icon fonts included so they can appear in emoji autosuggest dropdown import diceFont from '../../themes/fonts/iconFonts/diceFont.js'; @@ -59,6 +61,48 @@ mathParser.functions.signed = function (a) { if(a >= 0) return `+${a}`; return `${a}`; }; +// Add Roman numeral functions +mathParser.functions.toRomans = function (a) { + return romanize(a); +}; +mathParser.functions.toRomansUpper = function (a) { + return romanize(a).toUpperCase(); +}; +mathParser.functions.toRomansLower = function (a) { + return romanize(a).toLowerCase(); +}; +// Add character functions +mathParser.functions.toChar = function (a) { + if(a <= 0) return a; + const genChars = function (i) { + return (i > 26 ? genChars(Math.floor((i - 1) / 26)) : '') + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[(i - 1) % 26]; + }; + return genChars(a); +}; +mathParser.functions.toCharUpper = function (a) { + return mathParser.functions.toChar(a).toUpperCase(); +}; +mathParser.functions.toCharLower = function (a) { + return mathParser.functions.toChar(a).toLowerCase(); +}; +// Add word functions +mathParser.functions.toWords = function (a) { + return writtenNumber(a); +}; +mathParser.functions.toWordsUpper = function (a) { + return mathParser.functions.toWords(a).toUpperCase(); +}; +mathParser.functions.toWordsLower = function (a) { + return mathParser.functions.toWords(a).toLowerCase(); +}; +mathParser.functions.toWordsCaps = function (a) { + const words = mathParser.functions.toWords(a).split(' '); + return words.map((word)=>{ + return word.replace(/(?:^|\b|\s)(\w)/g, function(w, index) { + return index === 0 ? w.toLowerCase() : w.toUpperCase(); + }); + }).join(' '); +}; // Normalize variable names; trim edge spaces and shorten blocks of whitespace to 1 space const normalizeVarNames = (label)=>{ @@ -907,7 +951,13 @@ let globalPageNumber = 0; const Markdown = { marked : Marked, render : (rawBrewText, pageNumber=0)=>{ - globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order + const lastPageNumber = pageNumber > 0 ? globalVarsList[pageNumber - 1].HB_pageNumber.content : 0; + globalVarsList[pageNumber] = { //Reset global links for current page, to ensure values are parsed in order + 'HB_pageNumber' : { //Add document variables for this page + content : !isNaN(Number(lastPageNumber)) ? Number(lastPageNumber) + 1 : lastPageNumber, + resolved : true + } + }; varsQueue = []; //Could move into MarkedVariables() globalPageNumber = pageNumber; if(pageNumber==0) { diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js index 4c91d7d00..ea98fe56c 100644 --- a/tests/markdown/variables.test.js +++ b/tests/markdown/variables.test.js @@ -370,6 +370,30 @@ describe('Cross-page variables', ()=>{ const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('
two
one
\\pagetwo
'); }); + + it('Page numbering across pages : default', function() { + const source0 = `$[HB_pageNumber]\n\n`; + const source1 = `$[HB_pageNumber]\n\n`; + renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up + const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); + expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('1
\\page2
'); + }); + + it('Page numbering across pages : custom page number (Number)', function() { + const source0 = `[HB_pageNumber]:100\n\n$[HB_pageNumber]\n\n`; + const source1 = `$[HB_pageNumber]\n\n`; + renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up + const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); + expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('100
\\page101
'); + }); + + it('Page numbering across pages : custom page number (NaN)', function() { + const source0 = `[HB_pageNumber]:a\n\n$[HB_pageNumber]\n\n`; + const source1 = `$[HB_pageNumber]\n\n`; + renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up + const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); + expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('a
\\pagea
'); + }); }); describe('Math function parameter handling', ()=>{ @@ -434,5 +458,78 @@ describe('Regression Tests', ()=>{ const rendered = Markdown.render(source).trimReturns(); expect(rendered).toBe('
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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + expect(rendered).toBe('Words: Eighty Thousand And Eighty-Five
'); + }); }); \ No newline at end of file diff --git a/themes/V3/Blank/snippets.js b/themes/V3/Blank/snippets.js index 1b347dd07..c5198fd87 100644 --- a/themes/V3/Blank/snippets.js +++ b/themes/V3/Blank/snippets.js @@ -38,6 +38,11 @@ module.exports = [ icon : 'fas fa-sort-numeric-down', gen : '{{pageNumber,auto}}\n' }, + { + name : 'Variable Auto Page Number', + icon : 'fas fa-sort-numeric-down', + gen : '{{pageNumber $[HB_pageNumber]}}\n' + }, { name : 'Skip Page Number Increment this Page', icon : 'fas fa-xmark',