diff --git a/_atest.md b/_atest.md new file mode 100644 index 0000000..66ad4a2 --- /dev/null +++ b/_atest.md @@ -0,0 +1 @@ +WILL BE SEEN IN FINAL OUTPUT! diff --git a/_index.md b/_index.md index a17b154..6c02fce 100644 --- a/_index.md +++ b/_index.md @@ -1 +1 @@ -This content will go on the homepage. +_This content will go on the homepage._ diff --git a/_site.ori b/_site.ori deleted file mode 100644 index 05f533c..0000000 --- a/_site.ori +++ /dev/null @@ -1,49 +0,0 @@ -(all) => { - //put the file selection part in a closure, so that the list of all files is kept private. - pages: { - /* - list is all the files in current directory. - allfiles is all source files: ending with `.md` and not starting with `_`. - pubfiles is the subset containing the string `_pub`. - files selects either of these depending on the argument passed to this file. - */ - // allfiles: allfiles.sh() //reads MD files. - // pubfiles: pubfiles.sh(allfiles) - (list): .. - allfiles: Tree.filter(list, (val, key) => key.endsWith('.md') && !key.startsWith('_')) - pubfiles: Tree.filter(allfiles, (val, key) => key.includes('_pub')) - files: Tree.map( all ? allfiles : pubfiles, {value: (val) => Origami.document(val)}) - asHtml: Tree.map(files, (value) => Origami.mdHtml(value)) - → (values) => Tree.mapExtension(values, '.md→.html') - - /* - after converting to html, we can extract a 'summary' from the file. - With markdown, the title is taken from the YAML frontmatter, - and ends up in a separate field of the document. - So we dont have to worry about the title being part of the summary. - */ - withSummary: Tree.map(asHtml, extractSummary.js) - - /* - Removing private content coming below the html comment `` - Only remove this if in public mode. So if this file is called as `site.ori("all")`, - then final output is `withSummary`, otherwise it's `privateRemoved`. - */ - privateRemoved: Tree.map(withSummary, removePrivate.js) - final: (all ? withSummary : privateRemoved) - - }.final - - renderedPages: Tree.map(pages, page.ori) - - /* - assets are relative to the pubfiles directory - */ - css - - /* - Now, I think I have enough to build both the individual pages and the index page! - */ - index.html: indexPage.ori(pages) - ...renderedPages/ -} diff --git a/addFilenameData.js b/addFilenameData.js new file mode 100644 index 0000000..624db14 --- /dev/null +++ b/addFilenameData.js @@ -0,0 +1,6 @@ +import {parse} from './filenameparser.js'; + +export default (value, key) => { + const fnd = parse(key); + return {...value, fnd}; +} diff --git a/filenameparser.js b/filenameparser.js new file mode 100644 index 0000000..ab164df --- /dev/null +++ b/filenameparser.js @@ -0,0 +1,728 @@ +// @generated by Peggy 5.1.0. +// +// https://peggyjs.org/ + + +class peg$SyntaxError extends SyntaxError { + constructor(message, expected, found, location) { + super(message); + this.expected = expected; + this.found = found; + this.location = location; + this.name = "SyntaxError"; + } + + format(sources) { + let str = "Error: " + this.message; + if (this.location) { + let src = null; + const st = sources.find(s => s.source === this.location.source); + if (st) { + src = st.text.split(/\r\n|\n|\r/g); + } + const s = this.location.start; + const offset_s = (this.location.source && (typeof this.location.source.offset === "function")) + ? this.location.source.offset(s) + : s; + const loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; + if (src) { + const e = this.location.end; + const filler = "".padEnd(offset_s.line.toString().length, " "); + const line = src[s.line - 1]; + const last = s.line === e.line ? e.column : line.length + 1; + const hatLen = (last - s.column) || 1; + str += "\n --> " + loc + "\n" + + filler + " |\n" + + offset_s.line + " | " + line + "\n" + + filler + " | " + "".padEnd(s.column - 1, " ") + + "".padEnd(hatLen, "^"); + } else { + str += "\n at " + loc; + } + } + return str; + } + + static buildMessage(expected, found) { + function hex(ch) { + return ch.codePointAt(0).toString(16).toUpperCase(); + } + + const nonPrintable = Object.prototype.hasOwnProperty.call(RegExp.prototype, "unicode") + ? new RegExp("[\\p{C}\\p{Mn}\\p{Mc}]", "gu") + : null; + function unicodeEscape(s) { + if (nonPrintable) { + return s.replace(nonPrintable, ch => "\\u{" + hex(ch) + "}"); + } + return s; + } + + function literalEscape(s) { + return unicodeEscape(s + .replace(/\\/g, "\\\\") + .replace(/"/g, "\\\"") + .replace(/\0/g, "\\0") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) + .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch))); + } + + function classEscape(s) { + return unicodeEscape(s + .replace(/\\/g, "\\\\") + .replace(/\]/g, "\\]") + .replace(/\^/g, "\\^") + .replace(/-/g, "\\-") + .replace(/\0/g, "\\0") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) + .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch))); + } + + const DESCRIBE_EXPECTATION_FNS = { + literal(expectation) { + return "\"" + literalEscape(expectation.text) + "\""; + }, + + class(expectation) { + const escapedParts = expectation.parts.map( + part => (Array.isArray(part) + ? classEscape(part[0]) + "-" + classEscape(part[1]) + : classEscape(part)) + ); + + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]" + (expectation.unicode ? "u" : ""); + }, + + any() { + return "any character"; + }, + + end() { + return "end of input"; + }, + + other(expectation) { + return expectation.description; + }, + }; + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + const descriptions = expected.map(describeExpectation); + descriptions.sort(); + + if (descriptions.length > 0) { + let j = 1; + for (let i = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + " or " + descriptions[1]; + + default: + return descriptions.slice(0, -1).join(", ") + + ", or " + + descriptions[descriptions.length - 1]; + } + } + + function describeFound(found) { + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; + } +} + +function peg$parse(input, options) { + options = options !== undefined ? options : {}; + + const peg$FAILED = {}; + const peg$source = options.grammarSource; + + const peg$startRuleFunctions = { + line: peg$parseline, + }; + let peg$startRuleFunction = peg$parseline; + + const peg$c0 = "."; + const peg$c1 = ":"; + const peg$c2 = "_"; + + const peg$r0 = /^[a-z0-9]/; + const peg$r1 = /^[^:.]/; + const peg$r2 = /^[a-p]/; + const peg$r3 = /^[^_.]/i; + + const peg$e0 = peg$literalExpectation(".", false); + const peg$e1 = peg$classExpectation([["a", "z"], ["0", "9"]], false, false, false); + const peg$e2 = peg$literalExpectation(":", false); + const peg$e3 = peg$classExpectation([":", "."], true, false, false); + const peg$e4 = peg$classExpectation([["a", "p"]], false, false, false); + const peg$e5 = peg$classExpectation(["_", "."], true, true, false); + const peg$e6 = peg$literalExpectation("_", false); + + function peg$f0(addr, name, tags, ext) {return { + addr: addr.value, + name: name.value, + tags: tags?.tags, + ext: ext.value +} } + function peg$f1(value) {return {type: "extension", value} } + function peg$f2(tags) {return {type: "tags", tags} } + function peg$f3(value) {return {type: "address", value} } + function peg$f4(value) {return {type: "name", value} } + let peg$currPos = options.peg$currPos | 0; + let peg$savedPos = peg$currPos; + const peg$posDetailsCache = [{ line: 1, column: 1 }]; + let peg$maxFailPos = peg$currPos; + let peg$maxFailExpected = options.peg$maxFailExpected || []; + let peg$silentFails = options.peg$silentFails | 0; + + let peg$result; + + if (options.startRule) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function offset() { + return peg$savedPos; + } + + function range() { + return { + source: peg$source, + start: peg$savedPos, + end: peg$currPos, + }; + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description, location) { + location = location !== undefined + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); + } + + function error(message, location) { + location = location !== undefined + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildSimpleError(message, location); + } + + function peg$getUnicode(pos = peg$currPos) { + const cp = input.codePointAt(pos); + if (cp === undefined) { + return ""; + } + return String.fromCodePoint(cp); + } + + function peg$literalExpectation(text, ignoreCase) { + return { type: "literal", text, ignoreCase }; + } + + function peg$classExpectation(parts, inverted, ignoreCase, unicode) { + return { type: "class", parts, inverted, ignoreCase, unicode }; + } + + function peg$anyExpectation() { + return { type: "any" }; + } + + function peg$endExpectation() { + return { type: "end" }; + } + + function peg$otherExpectation(description) { + return { type: "other", description }; + } + + function peg$computePosDetails(pos) { + let details = peg$posDetailsCache[pos]; + let p; + + if (details) { + return details; + } else { + if (pos >= peg$posDetailsCache.length) { + p = peg$posDetailsCache.length - 1; + } else { + p = pos; + while (!peg$posDetailsCache[--p]) {} + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column, + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + + return details; + } + } + + function peg$computeLocation(startPos, endPos, offset) { + const startPosDetails = peg$computePosDetails(startPos); + const endPosDetails = peg$computePosDetails(endPos); + + const res = { + source: peg$source, + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column, + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column, + }, + }; + if (offset && peg$source && (typeof peg$source.offset === "function")) { + res.start = peg$source.offset(res.start); + res.end = peg$source.offset(res.end); + } + return res; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + return new peg$SyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parseline() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parseaddr(); + if (s1 !== peg$FAILED) { + s2 = peg$parsename(); + if (s2 !== peg$FAILED) { + s3 = peg$parsetags(); + if (s3 === peg$FAILED) { + s3 = null; + } + s4 = peg$parseext(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f0(s1, s2, s3, s4); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseext() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s4 = peg$c0; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + if (s4 !== peg$FAILED) { + s5 = []; + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + if (s6 !== peg$FAILED) { + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + } + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s4 = peg$c0; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + if (s4 !== peg$FAILED) { + s5 = []; + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + if (s6 !== peg$FAILED) { + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = input.charAt(peg$currPos); + if (peg$r0.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + } + } else { + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f1(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsetags() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsetag(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 58) { + s4 = peg$c1; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } + if (s4 !== peg$FAILED) { + s4 = peg$parsetag(); + if (s4 === peg$FAILED) { + peg$currPos = s3; + s3 = peg$FAILED; + } else { + s3 = s4; + } + } else { + s3 = s4; + } + } + peg$savedPos = s0; + s0 = peg$f2(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsetag() { + let s0, s1, s2; + + s0 = peg$currPos; + s1 = []; + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s0 = input.substring(s0, peg$currPos); + } else { + s0 = s1; + } + + return s0; + } + + function peg$parseaddr() { + let s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = []; + s3 = input.charAt(peg$currPos); + if (peg$r2.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = input.charAt(peg$currPos); + if (peg$r2.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = input.substring(s1, peg$currPos); + } else { + s1 = s2; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f3(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsename() { + let s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + s3 = []; + s4 = input.charAt(peg$currPos); + if (peg$r3.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + if (s4 !== peg$FAILED) { + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = input.charAt(peg$currPos); + if (peg$r3.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + } + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s2 = input.substring(s2, peg$currPos); + } else { + s2 = s3; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f4(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parse_() { + let s0; + + if (input.charCodeAt(peg$currPos) === 95) { + s0 = peg$c2; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } + + return s0; + } + + peg$result = peg$startRuleFunction(); + + const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length); + function peg$throw() { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? peg$getUnicode(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } + if (options.peg$library) { + return /** @type {any} */ ({ + peg$result, + peg$currPos, + peg$FAILED, + peg$maxFailExpected, + peg$maxFailPos, + peg$success, + peg$throw: peg$success ? undefined : peg$throw, + }); + } + if (peg$success) { + return peg$result; + } else { + peg$throw(); + } +} + +const peg$allowedStartRules = [ + "line" +]; + +export { + peg$allowedStartRules as StartRules, + peg$SyntaxError as SyntaxError, + peg$parse as parse +}; diff --git a/filenameparser.pegjs b/filenameparser.pegjs new file mode 100644 index 0000000..4efe4d8 --- /dev/null +++ b/filenameparser.pegjs @@ -0,0 +1,24 @@ +/* +filename syntax: addr_filename[_colon:separated:tags].ext[.ext] +addr: sequence of one or more a-p. String. +filename: anything except underscore or period. String. +tags: colon-separated list of tags. Array of Strings. +ext: period, then a-z0-9. Multiple allowed. String, including periods. +*/ +line = addr:addr name:name tags:tags? ext:ext {return { + addr: addr.value, + name: name.value, + tags: tags?.tags, + ext: ext.value +}} +ext = value:$(("." [a-z0-9]+))+ {return {type: "extension", value}} + +//tags are separated by colon to allow dashes in values for e.g. dates. +tags = _ tags:tag|..,":"| {return {type: "tags", tags}} +tag = $[^:.]+ +addr = value:$[a-p]+ {return {type: "address", value}} + +//a limitation: filenames may not contain periods. +//Otherwise it gets more complicated to handle extensions. +name = _ value:$[^_.]i+ {return {type: "name", value}} +_ = "_" diff --git a/site.ori b/site.ori index a2eff33..6d23e85 100644 --- a/site.ori +++ b/site.ori @@ -1,14 +1,65 @@ (all) => { - /* - list is all the files in current directory. - allfiles: ending with `.md` and not starting with `_`. - pubfiles: the subset containing the string `_pub`. - files selects either of these depending on the argument passed to this file. - */ - (list): .. - allfiles: Tree.filter(list, - (val, key) => key.endsWith('.md') && !key.startsWith('_')) - pubfiles: Tree.filter(allfiles, - (val, key) => key.includes('_pub')) + //put the file selection part in a closure, so that the list of all files is kept private. + pages: { + /* + list is all the files in current directory. + allfiles is all source files: ending with `.md` and not starting with `_`. + pubfiles is the subset containing the string `_pub`. + files selects either of these depending on the argument passed to this file. + NOTES: + + - if markdown files are empty, Origami.document() will error. + */ + + /* + this is the external filename method. + */ + // allfiles: allfiles.sh() //reads MD files. + // pubfiles: pubfiles.sh(allfiles) + (list): .. + allfiles: Tree.filter(list, (val, key) => key.endsWith('.md') && !key.startsWith('_')) + → (items) => Tree.map(items, {value: (value) => Origami.document(value)}) //if I don't use the verbose syntax I get the filename characters as part of value! + → (items) => Tree.map(items, { value: (value, key) => addFilenameData.js(value, key)}) + + pubfiles: Tree.filter(allfiles, (val, key) => val.fnd.tags?.includes('pub')) + + /* + Now convert to html. + */ + asHtml: Tree.map(all ? allfiles: pubfiles, { + value: (value) => Origami.mdHtml(value) + key: (value, key) => `${value.fnd.name}.html` + }) + + /* + after converting to html, we can extract a 'summary' from the file. + With markdown, the title is taken from the YAML frontmatter, + and ends up in a separate field of the document. + So we dont have to worry about the title being part of the summary. + */ + withSummary: Tree.map(asHtml, extractSummary.js) + + /* + Removing private content coming below the html comment `` + Only remove this if in public mode. So if this file is called as `site.ori("all")`, + then final output is `withSummary`, otherwise it's `privateRemoved`. + */ + privateRemoved: Tree.map(withSummary, removePrivate.js) + final: (all ? withSummary : privateRemoved) + + }.final + + renderedPages: Tree.map(pages, page.ori) + + /* + assets are relative to the pubfiles directory + */ + css + + /* + Now, I think I have enough to build both the individual pages and the index page! + */ + index.html: indexPage.ori(pages) + ...renderedPages/ }