diff options
-rw-r--r-- | src/content/dependencies/transformContent.js | 246 | ||||
-rwxr-xr-x | src/upd8.js | 6 | ||||
-rw-r--r-- | src/util/replacer.js | 284 | ||||
-rw-r--r-- | src/util/transform-content.js | 452 | ||||
-rw-r--r-- | src/write/bind-utilities.js | 29 | ||||
-rw-r--r-- | src/write/build-modes/live-dev-server.js | 17 | ||||
-rw-r--r-- | tap-snapshots/test/snapshot/transformContent.js.test.cjs | 46 | ||||
-rw-r--r-- | test/snapshot/transformContent.js | 60 |
8 files changed, 462 insertions, 678 deletions
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 03bd7bcc..2c0a8e41 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -2,7 +2,115 @@ import {marked} from 'marked'; import {bindFind} from '../../util/find.js'; import {parseInput} from '../../util/replacer.js'; -import {replacerSpec} from '../../util/transform-content.js'; + +export const replacerSpec = { + album: { + find: 'album', + link: 'album', + }, + 'album-commentary': { + find: 'album', + link: 'albumCommentary', + }, + 'album-gallery': { + find: 'album', + link: 'albumGallery', + }, + artist: { + find: 'artist', + link: 'artist', + }, + 'artist-gallery': { + find: 'artist', + link: 'artistGallery', + }, + 'commentary-index': { + find: null, + link: 'commentaryIndex', + }, + date: { + find: null, + value: (ref) => new Date(ref), + html: (date, {html, language}) => + html.tag('time', + {datetime: date.toString()}, + language.formatDate(date)), + }, + 'flash-index': { + find: null, + link: 'flashIndex', + }, + flash: { + find: 'flash', + link: 'flash', + transformName(name, node, input) { + const nextCharacter = input[node.iEnd]; + const lastCharacter = name[name.length - 1]; + if (![' ', '\n', '<'].includes(nextCharacter) && lastCharacter === '.') { + return name.slice(0, -1); + } else { + return name; + } + }, + }, + group: { + find: 'group', + link: 'groupInfo', + }, + 'group-gallery': { + find: 'group', + link: 'groupGallery', + }, + home: { + find: null, + link: 'home', + }, + 'listing-index': { + find: null, + link: 'listingIndex', + }, + listing: { + find: 'listing', + link: 'listing', + }, + media: { + find: null, + link: 'media', + }, + 'news-index': { + find: null, + link: 'newsIndex', + }, + 'news-entry': { + find: 'newsEntry', + link: 'newsEntry', + }, + root: { + find: null, + link: 'root', + }, + site: { + find: null, + link: 'site', + }, + static: { + find: 'staticPage', + link: 'staticPage', + }, + string: { + find: null, + value: (ref) => ref, + html: (ref, {language, args}) => language.$(ref, args), + }, + tag: { + find: 'artTag', + link: 'tag', + }, + track: { + find: 'track', + link: 'track', + }, +}; const linkThingRelationMap = { album: 'linkAlbum', @@ -43,9 +151,10 @@ export default { ...Object.values(linkThingRelationMap), ...Object.values(linkValueRelationMap), ...Object.values(linkIndexRelationMap), + 'image', ], - extraDependencies: ['html', 'language', 'wikiData'], + extraDependencies: ['html', 'language', 'to', 'wikiData'], sprawl(wikiData, content) { const find = bindFind(wikiData); @@ -140,14 +249,16 @@ export default { nodes: sprawl.nodes .map(node => { - // Replace link nodes with a stub. It'll be replaced (by position) - // with an item from relations. - if (node.type === 'link') { - return {type: 'link'}; + switch (node.type) { + // Replace link nodes with a stub. It'll be replaced (by position) + // with an item from relations. + case 'link': + return {type: 'link'}; + + // Other nodes will get processed in generate. + default: + return node; } - - // Other nodes will get processed in generate. - return node; }), }; }, @@ -181,6 +292,12 @@ export default { return relationOrPlaceholder(node, linkIndexRelationMap[key]); } }), + + images: + nodes + .filter(({type}) => type === 'image') + .filter(({inline}) => !inline) + .map(() => relation('image')), }; }, @@ -200,63 +317,96 @@ export default { }, }, - generate(data, relations, slots, {html, language}) { + generate(data, relations, slots, {html, language, to}) { let linkIndex = 0; + let imageIndex = 0; // This array contains only straight text and link nodes, which are directly // representable in html (so no further processing is needed on the level of // individual nodes). const contentFromNodes = data.nodes.map(node => { - if (node.type === 'text') { - return {type: 'text', data: node.data}; - } + switch (node.type) { + case 'text': + return {type: 'text', data: node.data}; + + case 'image': { + const src = + (node.src.startsWith('media/') + ? to('media.path', node.src.slice('media/'.length)) + : node.src); + + const {width, height} = node; + + if (node.inline) { + return { + type: 'image', + data: + html.tag('img', {src, width, height}), + }; + } - if (node.type === 'link') { - const linkNode = relations.links[linkIndex++]; - if (linkNode.type === 'text') { - return {type: 'text', data: linkNode.data}; + const image = relations.images[imageIndex++]; + + return { + type: 'image', + data: + image.slots({ + src, + link: true, + width: width ?? null, + height: height ?? null, + }), + }; } - const {link, label, hash, toIndex} = linkNode; - - return { - type: 'link', - data: - (toIndex - ? link.slots({content: label, hash}) - : link.slots({ - content: label, hash, - preferShortName: slots.preferShortLinkNames, - })), - }; - } + case 'link': { + const linkNode = relations.links[linkIndex++]; + if (linkNode.type === 'text') { + return {type: 'text', data: linkNode.data}; + } - if (node.type === 'tag') { - const {replacerKey, replacerValue} = node.data; + const {link, label, hash, toIndex} = linkNode; + + return { + type: 'link', + data: + (toIndex + ? link.slots({content: label, hash}) + : link.slots({ + content: label, hash, + preferShortName: slots.preferShortLinkNames, + })), + }; + } - const spec = replacerSpec[replacerKey]; + case 'tag': { + const {replacerKey, replacerValue} = node.data; - if (!spec) { - return getPlaceholder(node, data.content); - } + const spec = replacerSpec[replacerKey]; - const {value: valueFn, html: htmlFn} = spec; + if (!spec) { + return getPlaceholder(node, data.content); + } - const value = - (valueFn - ? valueFn(replacerValue) - : replacerValue); + const {value: valueFn, html: htmlFn} = spec; - const contents = - (htmlFn - ? htmlFn(value, {html, language}) - : value); + const value = + (valueFn + ? valueFn(replacerValue) + : replacerValue); - return {type: 'text', data: contents}; - } + const contents = + (htmlFn + ? htmlFn(value, {html, language}) + : value); + + return {type: 'text', data: contents}; + } - return getPlaceholder(node, data.content); + default: + return getPlaceholder(node, data.content); + } }); // In single-link mode, return the link node exactly as is - exposing diff --git a/src/upd8.js b/src/upd8.js index 366dc21b..11ede7b6 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -63,9 +63,7 @@ import find from './util/find.js'; import {findFiles} from './util/io.js'; import link from './util/link.js'; import {isMain, traverse} from './util/node-utils.js'; -import {validateReplacerSpec} from './util/replacer.js'; import {empty, showAggregate, withEntries} from './util/sugar.js'; -import {replacerSpec} from './util/transform-content.js'; import {generateURLs} from './util/urls.js'; import {sortByName} from './util/wiki-data.js'; @@ -100,10 +98,6 @@ const BUILD_TIME = new Date(); const DEFAULT_STRINGS_FILE = 'strings-default.json'; -if (!validateReplacerSpec(replacerSpec, {find, link})) { - process.exit(); -} - async function main() { Error.stackTraceLimit = Infinity; diff --git a/src/util/replacer.js b/src/util/replacer.js index 50a90004..7240940d 100644 --- a/src/util/replacer.js +++ b/src/util/replacer.js @@ -1,23 +1,6 @@ import {logError, logWarn} from './cli.js'; import {escapeRegex} from './sugar.js'; -export function validateReplacerSpec(replacerSpec, {find, link}) { - let success = true; - - for (const [key, {link: linkKey, find: findKey, html}] of Object.entries(replacerSpec)) { - if (!html && !link[linkKey]) { - logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`; - success = false; - } - if (findKey && !find[findKey]) { - logError`The replacer spec ${key} has invalid find key ${findKey}! Specify it in find specs or fix typo.`; - success = false; - } - } - - return success; -} - // Syntax literals. const tagBeginning = '[['; const tagEnding = ']]'; @@ -292,13 +275,157 @@ function parseNodes(input, i, stopAt, textOnly) { return nodes; } +function parseAttributes(string) { + const attributes = Object.create(null); + + const skipWhitespace = i => { + if (!/\s/.test(string[i])) { + return i; + } + + const match = string.slice(i).match(/[^\s]/); + if (match) { + return i + match.index; + } + + return string.length; + }; + + for (let i = 0; i < string.length; ) { + i = skipWhitespace(i); + const aStart = i; + const aEnd = i + string.slice(i).match(/[\s=]|$/).index; + const attribute = string.slice(aStart, aEnd); + i = skipWhitespace(aEnd); + if (string[i] === '=') { + i = skipWhitespace(i + 1); + let end, endOffset; + if (string[i] === '"' || string[i] === "'") { + end = string[i]; + endOffset = 1; + i++; + } else { + end = '\\s'; + endOffset = 0; + } + const vStart = i; + const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index; + const value = string.slice(vStart, vEnd); + i = vEnd + endOffset; + attributes[attribute] = value; + } else { + attributes[attribute] = attribute; + } + } + + return ( + Object.fromEntries( + Object.entries(attributes) + .map(([key, val]) => [ + key, + val === 'true' + ? true + : val === 'false' + ? false + : val === key + ? true + : val, + ]))); +} + +export function postprocessImages(inputNodes) { + const outputNodes = []; + + let atStartOfLine = true; + + const lastNode = inputNodes[inputNodes.length - 1]; + + for (const node of inputNodes) { + if (node.type === 'tag') { + atStartOfLine = false; + } + + if (node.type === 'text') { + const imageRegexp = /<img (.*?)>/g; + + let match = null, parseFrom = 0; + while (match = imageRegexp.exec(node.data)) { + const previousText = node.data.slice(parseFrom, match.index); + outputNodes.push({type: 'text', data: previousText}); + parseFrom = match.index + match[0].length; + + const imageNode = {type: 'image'}; + const attributes = parseAttributes(match[1]); + + imageNode.src = attributes.src; + + if (previousText.endsWith('\n')) { + atStartOfLine = true; + } + + imageNode.inline = (() => { + // If we've already determined we're in the middle of a line, + // we're inline. (Of course!) + if (!atStartOfLine) { + return true; + } + + // If there's more text to go in this text node, and what's + // remaining doesn't start with a line break, we're inline. + if ( + parseFrom !== node.data.length && + node.data[parseFrom] !== '\n' + ) { + return true; + } + + // If we're at the end of this text node, but this text node + // isn't the last node overall, we're inline. + if ( + parseFrom === node.data.length && + node !== lastNode + ) { + return true; + } + + // If no other condition matches, this image is on its own line. + return false; + })(); + + if (attributes.width) imageNode.width = parseInt(attributes.width); + if (attributes.height) imageNode.height = parseInt(attributes.height); + + outputNodes.push(imageNode); + + // No longer at the start of a line after an image - there will at + // least be a text node with only '\n' before the next image that's + // on its own line. + atStartOfLine = false; + } + + if (parseFrom !== node.data.length) { + outputNodes.push({ + type: 'text', + data: node.data.slice(parseFrom), + }); + } + + continue; + } + + outputNodes.push(node); + } + + return outputNodes; +} + export function parseInput(input) { if (typeof input !== 'string') { throw new TypeError(`Expected input to be string, got ${input}`); } try { - return parseNodes(input, 0); + return postprocessImages(parseNodes(input, 0)); } catch (errorNode) { if (errorNode.type !== 'error') { throw errorNode; @@ -334,124 +461,3 @@ export function parseInput(input) { ].join('\n')); } } - -function evaluateTag(node, opts) { - const {find, input, language, link, replacerSpec, to} = opts; - - const source = input.slice(node.i, node.iEnd); - - const replacerKeyImplied = !node.data.replacerKey; - const replacerKey = replacerKeyImplied ? 'track' : node.data.replacerKey.data; - - if (!replacerSpec[replacerKey]) { - logWarn`The link ${source} has an invalid replacer key!`; - return source; - } - - const { - find: findKey, - link: linkKey, - value: valueFn, - html: htmlFn, - transformName, - } = replacerSpec[replacerKey]; - - const replacerValue = transformNodes(node.data.replacerValue, opts); - - const value = valueFn - ? valueFn(replacerValue) - : findKey - ? find[findKey]( - replacerKeyImplied ? replacerValue : replacerKey + `:` + replacerValue - ) - : { - directory: replacerValue, - name: null, - }; - - if (!value) { - logWarn`The link ${source} does not match anything!`; - return source; - } - - const enteredLabel = node.data.label && transformNode(node.data.label, opts); - - const label = - enteredLabel || - (transformName && transformName(value.name, node, input)) || - null; - - const hash = node.data.hash && transformNode(node.data.hash, opts); - - const args = - node.data.args && - Object.fromEntries( - node.data.args.map(({key, value}) => [ - transformNode(key, opts), - transformNodes(value, opts), - ]) - ); - - const fn = htmlFn ? htmlFn : link[linkKey]; - - try { - return fn(value, {text: label, hash, args, language, to}); - } catch (error) { - logError`The link ${source} failed to be processed: ${error}`; - return source; - } -} - -function transformNode(node, opts) { - if (!node) { - throw new Error('Expected a node!'); - } - - if (Array.isArray(node)) { - throw new Error('Got an array - use transformNodes here!'); - } - - switch (node.type) { - case 'text': - return node.data; - case 'tag': - return evaluateTag(node, opts); - default: - throw new Error(`Unknown node type ${node.type}`); - } -} - -function transformNodes(nodes, opts) { - if (!nodes || !Array.isArray(nodes)) { - throw new Error(`Expected an array of nodes! Got: ${nodes}`); - } - - return nodes.map((node) => transformNode(node, opts)).join(''); -} - -export function transformInline(input, { - replacerSpec, - find, - language, - link, - to, - wikiData, -}) { - if (!replacerSpec) throw new Error('Expected replacerSpec'); - if (!find) throw new Error('Expected find'); - if (!language) throw new Error('Expected language'); - if (!link) throw new Error('Expected link'); - if (!to) throw new Error('Expected to'); - if (!wikiData) throw new Error('Expected wikiData'); - - const nodes = parseInput(input); - return transformNodes(nodes, { - input, - find, - link, - replacerSpec, - language, - to, - wikiData, - }); -} diff --git a/src/util/transform-content.js b/src/util/transform-content.js deleted file mode 100644 index 454cb374..00000000 --- a/src/util/transform-content.js +++ /dev/null @@ -1,452 +0,0 @@ -// See also replacer.js, which covers the actual syntax parser and node -// interpreter. This file works with replacer.js to provide higher-level -// interfaces for converting various content found in wiki data to HTML for -// display on the site. - -export {transformInline} from './replacer.js'; - -export const replacerSpec = { - album: { - find: 'album', - link: 'album', - }, - 'album-commentary': { - find: 'album', - link: 'albumCommentary', - }, - 'album-gallery': { - find: 'album', - link: 'albumGallery', - }, - artist: { - find: 'artist', - link: 'artist', - }, - 'artist-gallery': { - find: 'artist', - link: 'artistGallery', - }, - 'commentary-index': { - find: null, - link: 'commentaryIndex', - }, - date: { - find: null, - value: (ref) => new Date(ref), - html: (date, {html, language}) => - html.tag('time', - {datetime: date.toString()}, - language.formatDate(date)), - }, - 'flash-index': { - find: null, - link: 'flashIndex', - }, - flash: { - find: 'flash', - link: 'flash', - transformName(name, node, input) { - const nextCharacter = input[node.iEnd]; - const lastCharacter = name[name.length - 1]; - if (![' ', '\n', '<'].includes(nextCharacter) && lastCharacter === '.') { - return name.slice(0, -1); - } else { - return name; - } - }, - }, - group: { - find: 'group', - link: 'groupInfo', - }, - 'group-gallery': { - find: 'group', - link: 'groupGallery', - }, - home: { - find: null, - link: 'home', - }, - 'listing-index': { - find: null, - link: 'listingIndex', - }, - listing: { - find: 'listing', - link: 'listing', - }, - media: { - find: null, - link: 'media', - }, - 'news-index': { - find: null, - link: 'newsIndex', - }, - 'news-entry': { - find: 'newsEntry', - link: 'newsEntry', - }, - root: { - find: null, - link: 'root', - }, - site: { - find: null, - link: 'site', - }, - static: { - find: 'staticPage', - link: 'staticPage', - }, - string: { - find: null, - value: (ref) => ref, - html: (ref, {language, args}) => language.$(ref, args), - }, - tag: { - find: 'artTag', - link: 'tag', - }, - track: { - find: 'track', - link: 'track', - }, -}; - -function splitLines(text) { - return text.split(/\r\n|\r|\n/); -} - -function joinLineBreaks(sourceLines) { - const outLines = []; - - let lineSoFar = ''; - for (let i = 0; i < sourceLines.length; i++) { - const line = sourceLines[i]; - lineSoFar += line; - if (!line.endsWith('<br>')) { - outLines.push(lineSoFar); - lineSoFar = ''; - } - } - - if (lineSoFar) { - outLines.push(lineSoFar); - } - - return outLines; -} - -function parseAttributes(string, {to}) { - const attributes = Object.create(null); - const skipWhitespace = (i) => { - const ws = /\s/; - if (ws.test(string[i])) { - const match = string.slice(i).match(/[^\s]/); - if (match) { - return i + match.index; - } else { - return string.length; - } - } else { - return i; - } - }; - - for (let i = 0; i < string.length; ) { - i = skipWhitespace(i); - const aStart = i; - const aEnd = i + string.slice(i).match(/[\s=]|$/).index; - const attribute = string.slice(aStart, aEnd); - i = skipWhitespace(aEnd); - if (string[i] === '=') { - i = skipWhitespace(i + 1); - let end, endOffset; - if (string[i] === '"' || string[i] === "'") { - end = string[i]; - endOffset = 1; - i++; - } else { - end = '\\s'; - endOffset = 0; - } - const vStart = i; - const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index; - const value = string.slice(vStart, vEnd); - i = vEnd + endOffset; - if (attribute === 'src' && value.startsWith('media/')) { - attributes[attribute] = to('media.path', value.slice('media/'.length)); - } else { - attributes[attribute] = value; - } - } else { - attributes[attribute] = attribute; - } - } - return Object.fromEntries( - Object.entries(attributes).map(([key, val]) => [ - key, - val === 'true' - ? true - : val === 'false' - ? false - : val === key - ? true - : val, - ]) - ); -} - -function unbound_transformMultiline(text, { - img, - to, - transformInline, - - thumb = null, -}) { - // Heck yes, HTML magics. - - text = transformInline(text.trim()); - - const outLines = []; - - const indentString = ' '.repeat(4); - - let levelIndents = []; - const openLevel = (indent) => { - // opening a sublist is a pain: to be semantically *and* visually - // correct, we have to append the <ul> at the end of the existing - // previous <li> - const previousLine = outLines[outLines.length - 1]; - if (previousLine?.endsWith('</li>')) { - // we will re-close the <li> later - outLines[outLines.length - 1] = previousLine.slice(0, -5) + ' <ul>'; - } else { - // if the previous line isn't a list item, this is the opening of - // the first list level, so no need for indent - outLines.push('<ul>'); - } - levelIndents.push(indent); - }; - const closeLevel = () => { - levelIndents.pop(); - if (levelIndents.length) { - // closing a sublist, so close the list item containing it too - outLines.push(indentString.repeat(levelIndents.length) + '</ul></li>'); - } else { - // closing the final list level! no need for indent here - outLines.push('</ul>'); - } - }; - - // okay yes we should support nested formatting, more than one blockquote - // layer, etc, but hear me out here: making all that work would basically - // be the same as implementing an entire markdown converter, which im not - // interested in doing lol. sorry!!! - let inBlockquote = false; - - let lines = splitLines(text); - lines = joinLineBreaks(lines); - for (let line of lines) { - const imageLine = line.startsWith('<img'); - line = line.replace(/<img (.*?)>/g, (match, attributes) => - img({ - lazy: true, - link: true, - thumb, - ...parseAttributes(attributes, {to}), - }) - ); - - let indentThisLine = 0; - let lineContent = line; - let lineTag = 'p'; - - const listMatch = line.match(/^( *)- *(.*)$/); - if (listMatch) { - // is a list item! - if (!levelIndents.length) { - // first level is always indent = 0, regardless of actual line - // content (this is to avoid going to a lesser indent than the - // initial level) - openLevel(0); - } else { - // find level corresponding to indent - const indent = listMatch[1].length; - let i; - for (i = levelIndents.length - 1; i >= 0; i--) { - if (levelIndents[i] <= indent) break; - } - // note: i cannot equal -1 because the first indentation level - // is always 0, and the minimum indentation is also 0 - if (levelIndents[i] === indent) { - // same indent! return to that level - while (levelIndents.length - 1 > i) closeLevel(); - // (if this is already the current level, the above loop - // will do nothing) - } else if (levelIndents[i] < indent) { - // lesser indent! branch based on index - if (i === levelIndents.length - 1) { - // top level is lesser: add a new level - openLevel(indent); - } else { - // lower level is lesser: return to that level - while (levelIndents.length - 1 > i) closeLevel(); - } - } - } - // finally, set variables for appending content line - indentThisLine = levelIndents.length; - lineContent = listMatch[2]; - lineTag = 'li'; - } else { - // not a list item! close any existing list levels - while (levelIndents.length) closeLevel(); - - // like i said, no nested shenanigans - quotes only appear outside - // of lists. sorry! - const quoteMatch = line.match(/^> *(.*)$/); - if (quoteMatch) { - // is a quote! open a blockquote tag if it doesnt already exist - if (!inBlockquote) { - inBlockquote = true; - outLines.push('<blockquote>'); - } - indentThisLine = 1; - lineContent = quoteMatch[1]; - } else if (inBlockquote) { - // not a quote! close a blockquote tag if it exists - inBlockquote = false; - outLines.push('</blockquote>'); - } - - // let some escaped symbols display as the normal symbol, since the - // point of escaping them is just to avoid having them be treated as - // syntax markers! - if (lineContent.match(/( *)\\-/)) { - lineContent = lineContent.replace('\\-', '-'); - } else if (lineContent.match(/( *)\\>/)) { - lineContent = lineContent.replace('\\>', '>'); - } - } - - if (lineTag === 'p') { - // certain inline element tags should still be postioned within a - // paragraph; other elements (e.g. headings) should be added as-is - const elementMatch = line.match(/^<(.*?)[ >]/); - if ( - elementMatch && - !imageLine && - ![ - 'a', - 'abbr', - 'b', - 'bdo', - 'br', - 'cite', - 'code', - 'data', - 'datalist', - 'del', - 'dfn', - 'em', - 'i', - 'img', - 'ins', - 'kbd', - 'mark', - 'output', - 'picture', - 'q', - 'ruby', - 'samp', - 'small', - 'span', - 'strong', - 'sub', - 'sup', - 'svg', - 'time', - 'var', - 'wbr', - ].includes(elementMatch[1]) - ) { - lineTag = ''; - } - - // for sticky headings! - if (elementMatch && elementMatch[1] === 'h2') { - lineContent = lineContent.replace(/<h2(.*?)>/g, (match, attributes) => { - const parsedAttributes = parseAttributes(attributes, {to}); - return `<h2 ${html.attributes({ - ...parsedAttributes, - class: [...parsedAttributes.class?.split(' ') ?? [], 'content-heading'], - })}>`; - }); - } - } - - let pushString = indentString.repeat(indentThisLine); - if (lineTag) { - pushString += `<${lineTag}>${lineContent}</${lineTag}>`; - } else { - pushString += lineContent; - } - outLines.push(pushString); - } - - // after processing all lines... - - // if still in a list, close all levels - while (levelIndents.length) closeLevel(); - - // if still in a blockquote, close its tag - if (inBlockquote) { - inBlockquote = false; - outLines.push('</blockquote>'); - } - - return outLines.join('\n'); -} - -function unbound_transformLyrics(text, { - transformInline, - transformMultiline, -}) { - // Different from transformMultiline 'cuz it joins multiple lines together - // with line 8reaks (<br>); transformMultiline treats each line as its own - // complete paragraph (or list, etc). - - // If it looks like old data, then like, oh god. - // Use the normal transformMultiline tool. - if (text.includes('<br')) { - return transformMultiline(text); - } - - text = transformInline(text.trim()); - - let buildLine = ''; - const addLine = () => outLines.push(`<p>${buildLine}</p>`); - const outLines = []; - for (const line of text.split('\n')) { - if (line.length) { - if (buildLine.length) { - buildLine += '<br>'; - } - buildLine += line; - } else if (buildLine.length) { - addLine(); - buildLine = ''; - } - } - if (buildLine.length) { - addLine(); - } - return outLines.join('\n'); -} - -export { - unbound_transformLyrics as transformLyrics, - unbound_transformMultiline as transformMultiline -} diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index d6053353..2ddc2b38 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -4,13 +4,6 @@ import chroma from 'chroma-js'; -import { - replacerSpec, - transformInline, - // transformLyrics, - // transformMultiline, -} from '../util/transform-content.js'; - import * as html from '../util/html.js'; import {bindOpts} from '../util/sugar.js'; @@ -57,28 +50,6 @@ export function bindUtilities({ bound.find = bindFind(wikiData, {mode: 'warn'}); - bound.transformInline = bindOpts(transformInline, { - find: bound.find, - link: bound.link, - replacerSpec, - language, - to, - wikiData, - }); - - /* - bound.transformMultiline = bindOpts(transformMultiline, { - img: bound.img, - to, - transformInline: bound.transformInline, - }); - - bound.transformLyrics = bindOpts(transformLyrics, { - transformInline: bound.transformInline, - transformMultiline: bound.transformMultiline, - }); - */ - /* bound.generateNavigationLinks = bindOpts(generateNavigationLinks, { link: bound.link, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index d7c33d87..78c3928f 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -56,6 +56,11 @@ export function getCLIOptions() { return true; }, }, + + 'quiet-responses': { + help: `Disables outputting [200] and [404] responses in the server log`, + type: 'flag', + }, }; } @@ -86,6 +91,7 @@ export async function go({ const host = cliOptions['host'] ?? defaultHost; const port = parseInt(cliOptions['port'] ?? defaultPort); + const quietResponses = cliOptions['quiet-responses'] ?? false; const contentDependenciesWatcher = await watchContentDependencies(); const {contentDependencies: allContentDependencies} = contentDependenciesWatcher; @@ -167,7 +173,7 @@ export async function go({ }); response.writeHead(200, contentTypeJSON); response.end(json); - console.log(`${requestHead} [200] /data.json`); + if (!quietResponses) console.log(`${requestHead} [200] /data.json`); } catch (error) { response.writeHead(500, contentTypeJSON); response.end({error: `Internal error serializing wiki JSON`}); @@ -263,7 +269,7 @@ export async function go({ await pipeline( createReadStream(filePath), response); - console.log(`${requestHead} [200] ${pathname}`); + if (!quietResponses) console.log(`${requestHead} [200] ${pathname}`); } catch (error) { response.writeHead(500, contentTypePlain); response.end(`Failed during file-to-response pipeline`); @@ -281,7 +287,7 @@ export async function go({ if (!Object.hasOwn(urlToPageMap, pathnameKey)) { response.writeHead(404, contentTypePlain); response.end(`No page found for: ${pathnameKey}\n`); - console.log(`${requestHead} [404] ${pathname}`); + if (!quietResponses) console.log(`${requestHead} [404] ${pathname}`); return; } @@ -462,7 +468,7 @@ export async function go({ const pageHTML = topLevelResult.toString(); - console.log(`${requestHead} [200] ${pathname}`); + if (!quietResponses) console.log(`${requestHead} [200] ${pathname}`); response.writeHead(200, contentTypeHTML); response.end(pageHTML); } catch (error) { @@ -492,6 +498,9 @@ export async function go({ server.on('listening', () => { logInfo`${'All done!'} Listening at: ${address}`; logInfo`Press ^C here (control+C) to stop the server and exit.`; + if (quietResponses) { + logInfo`Suppressing [200] and [404] response logging.`; + } }); server.listen(port, host); diff --git a/tap-snapshots/test/snapshot/transformContent.js.test.cjs b/tap-snapshots/test/snapshot/transformContent.js.test.cjs new file mode 100644 index 00000000..a59f8b5d --- /dev/null +++ b/tap-snapshots/test/snapshot/transformContent.js.test.cjs @@ -0,0 +1,46 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > inline images 1`] = ` +<p><img src="snooping.png"> as USUAL...</p> +<p>What do you know? <img src="cowabunga.png" width="24" height="32"></p> +<p><a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">I'm on the left.</a><img src="im-on-the-right.jpg"></p> +<p><img src="im-on-the-left.jpg"><a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">I'm on the right.</a></p> +<p>Media time! <img src="to-media.path/misc/interesting.png"> Oh yeah!</p> +<p><img src="must.png"><img src="stick.png"><img src="together.png"></p> +<p>And... all done! <img src="end-of-source.png"></p> + +` + +exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > links to a thing 1`] = ` +<p>This is <a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">my favorite album</a>.</p> +<p>That's right, <a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">Cool Album</a>!</p> + +` + +exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > non-inline image #1 1`] = ` +<p><a class="box image-link" href="spark.png"><div class="image-container"><div class="image-inner-area"><img src="spark.png"></div></div></a></p> + +` + +exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > non-inline image #2 1`] = ` +<p>Rad.</p> +<p><a class="box image-link" href="spark.png"><div class="image-container"><div class="image-inner-area"><img src="spark.png"></div></div></a></p> + +` + +exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > non-inline image #3 1`] = ` +<p><a class="box image-link" href="spark.png"><div class="image-container"><div class="image-inner-area"><img src="spark.png"></div></div></a></p> +<p>Baller.</p> + +` + +exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > two text paragraphs 1`] = ` +<p>Hello, world!</p> +<p>Wow, this is very cool.</p> + +` diff --git a/test/snapshot/transformContent.js b/test/snapshot/transformContent.js new file mode 100644 index 00000000..f55ca4fa --- /dev/null +++ b/test/snapshot/transformContent.js @@ -0,0 +1,60 @@ +import t from 'tap'; +import {testContentFunctions} from '../lib/content-function.js'; + +testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => { + await evaluate.load(); + + const extraDependencies = { + wikiData: { + albumData: [ + {directory: 'cool-album', name: 'Cool Album', color: '#123456'}, + ], + }, + + getSizeOfImageFile: () => 0, + + to: (key, ...args) => `to-${key}/${args.join('/')}`, + }; + + const quickSnapshot = (message, content, slots) => + evaluate.snapshot(message, { + name: 'transformContent', + args: [content], + extraDependencies, + slots, + }); + + // TODO: Snapshots for different transformContent modes + + quickSnapshot( + 'two text paragraphs', + `Hello, world!\n` + + `Wow, this is very cool.`); + + quickSnapshot( + 'links to a thing', + `This is [[album:cool-album|my favorite album]].\n` + + `That's right, [[album:cool-album]]!`); + + quickSnapshot( + 'inline images', + `<img src="snooping.png"> as USUAL...\n` + + `What do you know? <img src="cowabunga.png" width="24" height="32">\n` + + `[[album:cool-album|I'm on the left.]]<img src="im-on-the-right.jpg">\n` + + `<img src="im-on-the-left.jpg">[[album:cool-album|I'm on the right.]]\n` + + `Media time! <img src="media/misc/interesting.png"> Oh yeah!\n` + + `<img src="must.png"><img src="stick.png"><img src="together.png">\n` + + `And... all done! <img src="end-of-source.png">`); + + quickSnapshot( + 'non-inline image #1', + `<img src="spark.png">`); + + quickSnapshot( + 'non-inline image #2', + `Rad.\n<img src="spark.png">`); + + quickSnapshot( + 'non-inline image #3', + `<img src="spark.png">\nBaller.`); +}); |