diff options
| -rw-r--r-- | src/common-util/sugar.js | 47 | ||||
| -rw-r--r-- | src/common-util/wiki-data.js | 88 | ||||
| -rw-r--r-- | src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js | 141 | ||||
| -rw-r--r-- | src/content/dependencies/generateContentEntry.js | 8 | ||||
| -rw-r--r-- | src/content/dependencies/generateContentEntryDate.js | 2 | ||||
| -rw-r--r-- | src/content/dependencies/generateLyricsEntry.js | 4 | ||||
| -rw-r--r-- | src/data/checks.js | 7 | ||||
| -rw-r--r-- | src/data/composite.js | 32 | ||||
| -rw-r--r-- | src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js | 29 | ||||
| -rw-r--r-- | src/data/things/content/ContentEntry.js | 53 | ||||
| -rw-r--r-- | src/static/css/search.css | 2 | ||||
| -rw-r--r-- | src/static/js/client/index.js | 2 | ||||
| -rw-r--r-- | src/static/js/client/sidebar-search.js | 85 | ||||
| -rw-r--r-- | src/strings-default.yaml | 4 |
14 files changed, 311 insertions, 193 deletions
diff --git a/src/common-util/sugar.js b/src/common-util/sugar.js index 4dd34785..c988156c 100644 --- a/src/common-util/sugar.js +++ b/src/common-util/sugar.js @@ -418,39 +418,28 @@ export function escapeRegex(string) { } // Adapted from here: https://emnudge.dev/notes/multiline-regex/ +// ...with a lot of changes export function re(...args) { - let flags = ''; - - const fn = (strings, ...substitutions) => { - strings = strings - .map(str => str.replace(/(?:\/\/.+)/gm, '')) - .map(str => str.replace(/\s+/g, '')); - - substitutions = substitutions - .map(sub => [sub].flat(Infinity)) - .map(sub => sub - .map(item => - (item instanceof RegExp - ? item.source - : item.toString()))) - .map(sub => sub.join('')); - - const source = - String.raw({raw: strings}, ...substitutions); - - return new RegExp(source, flags); - }; - - if ( - args.length === 1 && - typeof args[0] === 'string' && - args[0].match(/^[a-z]+$/) - ) { + let flags, parts; + if (args.length === 2) { flags = args[0]; - return fn; + parts = args[1]; + } else if (args.length === 1) { + flags = ''; + parts = args[0]; } else { - return fn(...args); + throw new Error(`Expected 1 or 2 arguments`); } + + const source = parts + .flat(Infinity) + .map(item => + (item instanceof RegExp + ? item.source + : item.toString())) + .join(''); + + return new RegExp(source, flags); } export function splitKeys(key) { diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js index 4c7f66f4..ff325b7a 100644 --- a/src/common-util/wiki-data.js +++ b/src/common-util/wiki-data.js @@ -1,6 +1,6 @@ // Utility functions for interacting with wiki data. -import {accumulateSum, chunkByConditions, empty, unique} from './sugar.js'; +import {accumulateSum, chunkByConditions, empty, re, unique} from './sugar.js'; import {sortByDate} from './sort.js'; // This is a duplicate binding of filterMultipleArrays that's included purely @@ -76,42 +76,48 @@ export function compareKebabCase(name1, name2) { // by slashes or dashes (only valid orders are MM/DD/YYYY and YYYY/MM/DD) // const dateRegex = groupName => - String.raw`(?<${groupName}>` + - String.raw`[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|` + - String.raw`[0-9]{1,2} [^,]*[0-9]{4,4}|` + - String.raw`[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4}` + - String.raw`)`; - -const contentEntryHeadingRegexRaw = - String.raw`^(?:` + - String.raw`(?:` + - String.raw`<i>(?<artists>.+?):<\/i>` + - String.raw`(?: \((?<annotation1>.*)\))?` + - String.raw`)` + - String.raw`|` + - String.raw`(?:` + - String.raw`@@ (?<annotation2>.*)` + - String.raw`)` + - String.raw`)$`; + re([ + `(?<${groupName}>`, + /[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}/, '|', + /[0-9]{1,2} [^,]*[0-9]{4,4}/, '|', + /[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4}/, + `)`, + ]); const contentEntryHeadingRegex = - new RegExp(contentEntryHeadingRegexRaw, 'gm'); - -const contentEntryAnnotationTailRegexRaw = - String.raw`(?:, |^)` + - - String.raw`(?:(?<dateKind>sometime|throughout|around) )?` + - String.raw`${dateRegex('date')}` + - String.raw`(?: ?- ?${dateRegex('secondDate')})?` + - - String.raw`(?: ?(?<= )` + - String.raw`(?<accessKind>captured|accessed) ${dateRegex('accessDate')}` + - String.raw`)?` + - - String.raw`$`; + re('gm', [ + '^(?:', + '(?:', + /<i>(?<artists>.+?):<\/i>/, + /(?: \((?<annotation1>.*)\))?/, + ')', + '|', + '(?:', + /@@ (?<annotation2>.*)/, + ')', + ')$', + ]); const contentEntryAnnotationTailRegex = - new RegExp(contentEntryAnnotationTailRegexRaw); + re([ + /(?:, |^)/, + + /(?:(?<dateKind>sometime|throughout|around) )?/, + dateRegex('date'), + + '(?:', + ' ?', + '-', + ' ?', + dateRegex('secondDate'), + ')?', + + '(?: ?(?<= )', + /(?<accessKind>captured|accessed)/, + ' ', + dateRegex('accessDate'), + ')?', + ]); export function* matchContentEntries(sourceText) { let workingEntry = null; @@ -593,7 +599,21 @@ export function* matchMarkdownLinks(markdownSource, {marked}) { } export function* matchInlineLinks(source) { - const plausibleLinkRegexp = /\b[a-z]*:\/\/[^ ]*?(?=(?:[,.!?]*)(?:\s|$))/gm; + const plausibleLinkRegexp = + re('gmi', [ + /\b[a-z]*:\/\//, + /.*?/, + + '(?=', + // Ordinary in-sentence punctuation doesn't terminate the + // un-greedy URL match above, but it shouldn't be counted + // as part of the link either, if it's at the end. + /(?:[,.!?]*)/, + + // Actual terminators. + /(?:\s|$|<br>)/, + ')', + ]); let plausibleMatch = null; while (plausibleMatch = plausibleLinkRegexp.exec(source)) { diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index 572eb982..6b603375 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -30,6 +30,10 @@ export default { flashAct, flash, + quoted: + !entry.headingArtists.includes(artist) && + entry.quotedArtists.includes(artist), + annotation: entry.annotation, annotationParts: entry.annotationParts, }, @@ -191,6 +195,11 @@ export default { query.chunks .map(({chunk}) => chunk .map(({itemType}) => itemType)), + + itemQuoted: + query.chunks + .map(({chunk}) => chunk + .map(({quoted}) => quoted)), }), generate: (data, relations, {html, language}) => @@ -206,6 +215,7 @@ export default { itemLinks: relations.itemLinks, itemAnnotations: relations.itemAnnotations, itemTypes: data.itemTypes, + itemQuoted: data.itemQuoted, }).map(({ chunk, chunkLink, @@ -215,64 +225,77 @@ export default { itemLinks, itemAnnotations, itemTypes, + itemQuoted, }) => - language.encapsulate('artistPage.creditList.entry', capsule => - (chunkType === 'album' - ? chunk.slots({ - mode: 'album', - link: chunkLink, - - list: - html.tag('ul', - stitchArrays({ - item: items, - link: itemLinks, - annotation: itemAnnotations, - type: itemTypes, - }).map(({item, link, annotation, type}) => - item.slots({ - // The citation slot, instead of annotation, gives commentary - // a specially custom look. - citation: - annotation.slots({ - mode: 'inline', - absorbPunctuationFollowingExternalLinks: false, - }), - - content: - (type === 'album' - ? html.tag('i', - language.$(capsule, 'album.commentary')) - : language.$(capsule, 'track', {track: link})), - }))), - }) - - : chunkType === 'flash-act' - ? chunk.slots({ - mode: 'flash', - link: chunkLink, - - list: - html.tag('ul', - stitchArrays({ - item: items, - link: itemLinks, - annotation: itemAnnotations, - }).map(({item, link, annotation}) => - item.slots({ - annotation: - (annotation - ? annotation.slots({ - mode: 'inline', - absorbPunctuationFollowingExternalLinks: false, - }) - : null), - - content: - language.$(capsule, 'flash', { - flash: link, - }), - }))), - }) - : null)))), + language.encapsulate('artistPage.creditList.entry', capsule => { + // The citation slot, instead of annotation, gives commentary + // a specially custom look. + const citations = + stitchArrays({annotation: itemAnnotations, quoted: itemQuoted}) + .map(({annotation, quoted}) => + language.encapsulate(capsule, workingCapsule => { + const workingOptions = {}; + + let any = false; + + annotation.setSlots({ + mode: 'inline', + absorbPunctuationFollowingExternalLinks: false, + }); + + if (!html.isBlank(annotation)) { + workingCapsule += '.citation'; + workingOptions.citation = annotation; + any = true; + } + + if (quoted) { + workingCapsule += '.quoted'; + any = true; + } + + if (any) { + return language.$(workingCapsule, workingOptions); + } else { + return html.blank(); + } + })); + + let contents; + + if (chunkType === 'album') { + chunk.setSlot('mode', 'album'); + contents = + stitchArrays({link: itemLinks, type: itemTypes}) + .map(({link, type}) => + (type === 'album' + ? html.tag('i', + language.$(capsule, 'album.commentary')) + : language.$(capsule, 'track', {track: link}))); + + } else if (chunkType === 'flash-act') { + chunk.setSlot('mode', 'flash'); + contents = + itemLinks.map(link => + language.$(capsule, 'flash', {flash: link})); + + } else { + throw new Error(`Gyeep!!`); + } + + chunk.setSlots({ + link: chunkLink, + + list: + html.tag('ul', + stitchArrays({ + item: items, + citation: citations, + content: contents, + }).map(({item, citation, content}) => + item.slots({citation, content}))), + }); + + return chunk; + }))), }; diff --git a/src/content/dependencies/generateContentEntry.js b/src/content/dependencies/generateContentEntry.js index c77f744a..40c637a3 100644 --- a/src/content/dependencies/generateContentEntry.js +++ b/src/content/dependencies/generateContentEntry.js @@ -3,14 +3,14 @@ import {empty} from '#sugar'; export default { relations: (relation, entry) => ({ artistLinks: - (!empty(entry.artists) && !entry.artistText - ? entry.artists + (!empty(entry.headingArtists) && !entry.headingArtistText + ? entry.headingArtists .map(artist => relation('linkArtist', artist)) : null), artistsContent: - (entry.artistText - ? relation('transformContent', entry.artistText) + (entry.headingArtistText + ? relation('transformContent', entry.headingArtistText) : null), annotationContent: diff --git a/src/content/dependencies/generateContentEntryDate.js b/src/content/dependencies/generateContentEntryDate.js index 845cb5ed..5255c7ea 100644 --- a/src/content/dependencies/generateContentEntryDate.js +++ b/src/content/dependencies/generateContentEntryDate.js @@ -35,7 +35,7 @@ export default { : entry.thing.isTrack && entry.thing.date === entry.thing.album.date && - entry.thing.style === 'single' + entry.thing.album.style === 'single' ? 'single' : entry.thing.isTrack && diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js index 15f84b27..0ecf319f 100644 --- a/src/content/dependencies/generateLyricsEntry.js +++ b/src/content/dependencies/generateLyricsEntry.js @@ -4,10 +4,10 @@ export default { relation('transformContent', entry.body), artistText: - relation('transformContent', entry.artistText), + relation('transformContent', entry.headingArtistText), artistLinks: - entry.artists + entry.headingArtists .filter(artist => artist.name !== 'HSMusic Wiki') // smh .map(artist => relation('linkArtist', artist)), diff --git a/src/data/checks.js b/src/data/checks.js index 0a0e7f52..01b5cf9e 100644 --- a/src/data/checks.js +++ b/src/data/checks.js @@ -352,6 +352,9 @@ export function filterReferenceErrors(wikiData, { switch (findFnKey) { case '_content': + // note: quoted artists ("Quoted Artists" field)`) currently + // not handled here at all. limitation of current code! oops! + if (value) { value = value.map(entry => @@ -721,13 +724,13 @@ export function reportContentTextErrors(wikiData, { const commentaryShape = { body: 'commentary body', - artistText: 'commentary artist text', + headingArtistText: 'commentary artist text', annotation: 'commentary annotation', }; const lyricsShape = { body: 'lyrics body', - artistText: 'lyrics artist text', + headingArtistText: 'lyrics artist text', annotation: 'lyrics annotation', }; diff --git a/src/data/composite.js b/src/data/composite.js index 8ac906c7..3b462ef5 100644 --- a/src/data/composite.js +++ b/src/data/composite.js @@ -10,17 +10,27 @@ import {TupleMap} from '#wiki-data'; const globalCompositeCache = {}; -const _valueIntoToken = shape => - (value = null) => - (value === null - ? Symbol.for(`hsmusic.composite.${shape}`) - : typeof value === 'string' - ? Symbol.for(`hsmusic.composite.${shape}:${value}`) - : { - symbol: Symbol.for(`hsmusic.composite.${shape.split('.')[0]}`), - shape, - value, - }); +const _valueIntoToken = shape => (value = null) => { + if (value === null) { + return Symbol.for(`hsmusic.composite.${shape}`); + } + + if (typeof value === 'string') { + return Symbol.for(`hsmusic.composite.${shape}:${value}`); + } + + if (typeof value === 'object') { + if (Object.values(value).some(isInputToken)) { + throw new TypeError(`Don't nest input tokens inside ${shape}()`); + } + } + + return { + symbol: Symbol.for(`hsmusic.composite.${shape.split('.')[0]}`), + shape, + value, + }; +}; export const input = _valueIntoToken('input'); input.symbol = Symbol.for('hsmusic.composite.input'); diff --git a/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js index 69da8c75..a6200ee8 100644 --- a/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js +++ b/src/data/composite/things/content/withExpressedOrImplicitArtistReferences.js @@ -8,16 +8,19 @@ export default templateCompositeFrom({ annotation: `withExpressedOrImplicitArtistReferences`, inputs: { - from: input({type: 'array', acceptsNull: true}), + fromExpressed: input({type: 'array', acceptsNull: true}), + fromContent: input({type: 'string', acceptsNull: true}), + + filterArtistTags: input({type: 'function', defaultValue: () => true}), }, outputs: ['#artistReferences'], steps: () => [ { - dependencies: [input('from')], + dependencies: [input('fromExpressed')], compute: (continuation, { - [input('from')]: expressedArtistReferences, + [input('fromExpressed')]: expressedArtistReferences, }) => (expressedArtistReferences ? continuation.raiseOutput({'#artistReferences': expressedArtistReferences}) @@ -25,12 +28,12 @@ export default templateCompositeFrom({ }, raiseOutputWithoutDependency({ - dependency: 'artistText', - output: input.value({'#artistReferences': null}), + dependency: input('fromContent'), + output: input.value({'#artistReferences': []}), }), withContentNodes({ - from: 'artistText', + from: input('fromContent'), }), withMappedList({ @@ -42,13 +45,19 @@ export default templateCompositeFrom({ '#mappedList': '#artistTagFilter', }), - withFilteredList({ - list: '#contentNodes', - filter: '#artistTagFilter', + withFilteredList('#contentNodes', '#artistTagFilter') + .outputs({'#filteredList': '#artistTags'}), + + withMappedList({ + list: '#artistTags', + map: input('filterArtistTags'), }).outputs({ - '#filteredList': '#artistTags', + '#mappedList': '#customFilter', }), + withFilteredList({list: '#artistTags', filter: '#customFilter'}) + .outputs({'#filteredList': '#artistTags'}), + withMappedList({ list: '#artistTags', map: input.value(node => diff --git a/src/data/things/content/ContentEntry.js b/src/data/things/content/ContentEntry.js index 04df303f..47f86622 100644 --- a/src/data/things/content/ContentEntry.js +++ b/src/data/things/content/ContentEntry.js @@ -1,5 +1,5 @@ import {input, V} from '#composite'; -import {transposeArrays} from '#sugar'; +import {transposeArrays, unique} from '#sugar'; import Thing from '#thing'; import {is, isDate, validateReferenceList} from '#validators'; import {parseDate} from '#yaml'; @@ -33,14 +33,43 @@ export class ContentEntry extends Thing { thing: thing(), - artists: [ + headingArtists: [ withExpressedOrImplicitArtistReferences({ - from: input.updateValue({ + fromExpressed: input.updateValue({ validate: validateReferenceList('artist'), }), + + fromContent: 'headingArtistText', + }), + + withResolvedReferenceList({ + list: '#artistReferences', + find: soupyFind.input('artist'), }), - exitWithoutDependency('#artistReferences', V([])), + exposeDependency('#resolvedReferenceList'), + ], + + quotedArtists: [ + exitWithoutDependency('body', V([])), + + { + dependencies: ['body'], + compute: (continuation, {body}) => continuation({ + ['#filterArtistTags']: node => + /(\n|^)> <i>$/.test(body.slice(0, node.i)) && + /^:<\/i>/.test(body.slice(node.iEnd)), + }), + }, + + withExpressedOrImplicitArtistReferences({ + fromExpressed: input.updateValue({ + validate: validateReferenceList('artist'), + }), + + fromContent: 'body', + filterArtistTags: '#filterArtistTags', + }), withResolvedReferenceList({ list: '#artistReferences', @@ -50,7 +79,7 @@ export class ContentEntry extends Thing { exposeDependency('#resolvedReferenceList'), ], - artistText: contentString(), + headingArtistText: contentString(), annotation: contentString(), @@ -119,6 +148,14 @@ export class ContentEntry extends Thing { isContentEntry: exposeConstant(V(true)), + artists: [ + { + dependencies: ['headingArtists', 'quotedArtists'], + compute: ({headingArtists, quotedArtists}) => + unique([...headingArtists, ...quotedArtists]), + }, + ], + annotationParts: [ withAnnotationPartNodeLists(), @@ -230,8 +267,10 @@ export class ContentEntry extends Thing { static [Thing.yamlDocumentSpec] = { fields: { - 'Artists': {property: 'artists'}, - 'Artist Text': {property: 'artistText'}, + 'Artists': {property: 'headingArtists'}, + 'Artist Text': {property: 'headingArtistText'}, + + 'Quoted Artists': {property: 'quotedArtists'}, 'Annotation': {property: 'annotation'}, diff --git a/src/static/css/search.css b/src/static/css/search.css index f421803b..a79fb20a 100644 --- a/src/static/css/search.css +++ b/src/static/css/search.css @@ -191,6 +191,8 @@ @layer layout { .wiki-search-context-container { padding: 2px 12px 4px; + padding-left: calc(12px + 1.2ch); + text-indent: -1.2ch; } } diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 16ebe89f..cd617bea 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -133,7 +133,7 @@ for (const module of modules) { break; case 'boolean': - formatRead = Boolean; + formatRead = value => value === 'true' ? true : false; formatWrite = String; break; diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index 8b29cf63..c39c38bc 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -38,6 +38,9 @@ export const info = { searchLabel: null, searchInput: null, + contextContainer: null, + contextBackLink: null, + progressRule: null, progressContainer: null, progressLabel: null, @@ -46,9 +49,6 @@ export const info = { failedRule: null, failedContainer: null, - contextContainer: null, - contextBackLink: null, - filterContainer: null, albumFilterLink: null, artistFilterLink: null, @@ -127,6 +127,7 @@ export const info = { activeQueryContextPageName: {type: 'string'}, activeQueryContextPagePathname: {type: 'string'}, activeQueryContextPageColor: {type: 'string'}, + zapActiveQueryContext: {type: 'boolean'}, activeQueryResults: { type: 'json', @@ -163,6 +164,8 @@ export function* bindSessionStorage() { yield 'activeQueryContextPageName'; yield 'activeQueryContextPagePathname'; yield 'activeQueryContextPageColor'; + yield 'zapActiveQueryContext'; + yield 'activeQueryResults'; yield 'activeFilterType'; yield 'resultsScrollOffset'; @@ -302,6 +305,25 @@ export function addInternalListeners() { export function mutatePageContent() { if (!info.searchBox) return; + // Context section + + info.contextContainer = + document.createElement('div'); + + info.contextContainer.classList.add('wiki-search-context-container'); + + info.contextBackLink = + document.createElement('a'); + + info.contextContainer.appendChild( + templateContent(info.backString, { + page: info.contextBackLink, + })); + + cssProp(info.contextContainer, 'display', 'none'); + + info.searchBox.appendChild(info.contextContainer); + // Progress section info.progressRule = @@ -355,25 +377,6 @@ export function mutatePageContent() { info.searchBox.appendChild(info.failedRule); info.searchBox.appendChild(info.failedContainer); - // Context section - - info.contextContainer = - document.createElement('div'); - - info.contextContainer.classList.add('wiki-search-context-container'); - - info.contextBackLink = - document.createElement('a'); - - info.contextContainer.appendChild( - templateContent(info.backString, { - page: info.contextBackLink, - })); - - cssProp(info.contextContainer, 'display', 'none'); - - info.searchBox.appendChild(info.contextContainer); - // Filter section info.filterContainer = @@ -750,6 +753,20 @@ function recordActiveQueryContext() { const {session} = info; if (document.documentElement.dataset.urlKey === 'localized.home') { + session.activeQueryContextPageName = null; + session.activeQueryContextPagePathname = null; + session.activeQueryContextPageColor = null; + session.zapActiveQueryContext = true; + return; + } + + // Zapping means subsequent searches don't record context. + if (session.zapActiveQueryContext) { + return; + } + + // We also don't overwrite existing context. + if (session.activeQueryContextPagePathname) { return; } @@ -780,11 +797,22 @@ function clearSidebarSearch() { state.searchStage = null; + clearActiveQuery(); + + hideSidebarSearchResults(); +} + +function clearActiveQuery() { + const {session} = info; + session.activeQuery = null; session.activeQueryResults = null; session.resultsScrollOffset = null; - hideSidebarSearchResults(); + session.activeQueryContextPageName = null; + session.activeQueryContextPagePathname = null; + session.activeQueryContextPageColor = null; + session.zapActiveQueryContext = false; } function clearSidebarFilter() { @@ -1537,16 +1565,7 @@ function considerRecallingRecentSidebarSearch() { } function forgetRecentSidebarSearch() { - const {session} = info; - - session.activeQuery = null; - - session.activeQueryContextPageName = null; - session.activeQueryContextPagePathname = null; - session.activeQueryContextPageColor = null; - - session.activeQueryResults = null; - + clearActiveQuery(); clearSidebarFilter(); } diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 54741239..bade35ac 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -1395,6 +1395,10 @@ artistPage: withCitation: "{ENTRY} ({CITATION})" + citation: "{CITATION}" + citation.quoted: "quoted: {CITATION}" + quoted: "quoted" + # rerelease: # Tracks which aren't the original release don't display co- # artists or contributors, and get dimmed a little compared |