« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/yaml.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/yaml.js')
-rw-r--r--src/data/yaml.js275
1 files changed, 231 insertions, 44 deletions
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 07dbe882..abe3bde2 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -8,9 +8,11 @@ import {inspect as nodeInspect} from 'node:util';
 import yaml from 'js-yaml';
 
 import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli';
+import {parseContentNodes, splitContentNodesAround} from '#replacer';
 import {sortByName} from '#sort';
 import Thing from '#thing';
 import thingConstructors from '#things';
+import {matchContentEntries, multipleLyricsDetectionRegex} from '#wiki-data';
 
 import {
   aggregateThrows,
@@ -85,7 +87,7 @@ function makeProcessDocument(thingConstructor, {
   //   ]
   //
   // ...means A can't coexist with B or C, B can't coexist with A or C, and
-  // C can't coexist iwth A, B, or D - but it's okay for D to coexist with
+  // C can't coexist with A, B, or D - but it's okay for D to coexist with
   // A or B.
   //
   invalidFieldCombinations = [],
@@ -180,9 +182,22 @@ function makeProcessDocument(thingConstructor, {
 
     const fieldCombinationErrors = [];
 
-    for (const {message, fields} of invalidFieldCombinations) {
+    for (const {message, fields: fieldsSpec} of invalidFieldCombinations) {
       const fieldsPresent =
-        presentFields.filter(field => fields.includes(field));
+        fieldsSpec.flatMap(fieldSpec => {
+          if (Array.isArray(fieldSpec)) {
+            const [field, match] = fieldSpec;
+            if (!presentFields.includes(field)) return [];
+            if (typeof match === 'function') {
+              return match(document[field]) ? [field] : [];
+            } else {
+              return document[field] === match ? [field] : [];
+            }
+          }
+
+          const field = fieldSpec;
+          return presentFields.includes(field) ? [field] : [];
+        });
 
       if (fieldsPresent.length >= 2) {
         const filteredDocument =
@@ -192,7 +207,10 @@ function makeProcessDocument(thingConstructor, {
             {preserveOriginalOrder: true});
 
         fieldCombinationErrors.push(
-          new FieldCombinationError(filteredDocument, message));
+          new FieldCombinationError(
+            filteredDocument,
+            fieldsSpec,
+            message));
 
         for (const field of Object.keys(filteredDocument)) {
           skippedFields.add(field);
@@ -248,7 +266,9 @@ function makeProcessDocument(thingConstructor, {
 
       // This variable would like to certify itself as "not into capitalism".
       let propertyValue =
-        (fieldSpecs[field].transform
+        (documentValue === null
+          ? null
+       : fieldSpecs[field].transform
           ? fieldSpecs[field].transform(documentValue, transformUtilities)
           : documentValue);
 
@@ -414,19 +434,36 @@ export class FieldCombinationAggregateError extends AggregateError {
 }
 
 export class FieldCombinationError extends Error {
-  constructor(fields, message) {
-    const fieldNames = Object.keys(fields);
+  constructor(filteredDocument, fieldsSpec, message) {
+    const fieldNames = Object.keys(filteredDocument);
 
     const fieldNamesText =
       fieldNames
-        .map(field => colors.red(field))
+        .map(field => {
+          if (fieldsSpec.includes(field)) {
+            return colors.red(field);
+          }
+
+          const match =
+            fieldsSpec
+              .find(fieldSpec =>
+                Array.isArray(fieldSpec) &&
+                fieldSpec[0] === field)
+              .at(1);
+
+          if (typeof match === 'function') {
+            return colors.red(`${field}: ${filteredDocument[field]}`);
+          } else {
+            return colors.red(`${field}: ${match}`);
+          }
+        })
         .join(', ');
 
     const mainMessage = `Don't combine ${fieldNamesText}`;
 
     const causeMessage =
       (typeof message === 'function'
-        ? message(fields)
+        ? message(filteredFields)
      : typeof message === 'string'
         ? message
         : null);
@@ -438,7 +475,7 @@ export class FieldCombinationError extends Error {
           : null),
     });
 
-    this.fields = fields;
+    this.fields = fieldNames;
   }
 }
 
@@ -609,49 +646,39 @@ export function parseContributors(entries) {
   });
 }
 
-export function parseAdditionalFiles(entries) {
+export function parseAdditionalFiles(entries, {subdoc, AdditionalFile}) {
   return parseArrayEntries(entries, item => {
     if (typeof item !== 'object') return item;
 
-    return {
-      title: item['Title'],
-      description: item['Description'] ?? null,
-      files: item['Files'],
-    };
+    return subdoc(AdditionalFile, item, {bindInto: 'thing'});
   });
 }
 
-export function parseAdditionalNames(entries) {
+export function parseAdditionalNames(entries, {subdoc, AdditionalName}) {
   return parseArrayEntries(entries, item => {
-    if (typeof item === 'object' && typeof item['Name'] === 'string')
-      return {
-        name: item['Name'],
-        annotation: item['Annotation'] ?? null,
-      };
+    if (typeof item === 'object') {
+      return subdoc(AdditionalName, item, {bindInto: 'thing'});
+    }
 
     if (typeof item !== 'string') return item;
 
     const match = item.match(extractAccentRegex);
     if (!match) return item;
 
-    return {
-      name: match.groups.main,
-      annotation: match.groups.accent ?? null,
+    const document = {
+      ['Name']: match.groups.main,
+      ['Annotation']: match.groups.accent ?? null,
     };
+
+    return subdoc(AdditionalName, document, {bindInto: 'thing'});
   });
 }
 
-export function parseSerieses(entries) {
+export function parseSerieses(entries, {subdoc, Series}) {
   return parseArrayEntries(entries, item => {
     if (typeof item !== 'object') return item;
 
-    return {
-      name: item['Name'],
-      description: item['Description'] ?? null,
-      albums: item['Albums'] ?? null,
-
-      showAlbumArtists: item['Show Album Artists'] ?? null,
-    };
+    return subdoc(Series, item, {bindInto: 'group'});
   });
 }
 
@@ -791,18 +818,24 @@ export function parseAnnotatedReferences(entries, {
 
 export function parseArtwork({
   single = false,
-  dimensionsFromThingProperty,
-  fileExtensionFromThingProperty,
-  dateFromThingProperty,
-  artistContribsFromThingProperty,
-  artistContribsArtistProperty,
+  thingProperty = null,
+  dimensionsFromThingProperty = null,
+  fileExtensionFromThingProperty = null,
+  dateFromThingProperty = null,
+  artistContribsFromThingProperty = null,
+  artistContribsArtistProperty = null,
+  artTagsFromThingProperty = null,
+  referencedArtworksFromThingProperty = null,
 }) {
   const provide = {
+    thingProperty,
     dimensionsFromThingProperty,
     fileExtensionFromThingProperty,
     dateFromThingProperty,
     artistContribsFromThingProperty,
     artistContribsArtistProperty,
+    artTagsFromThingProperty,
+    referencedArtworksFromThingProperty,
   };
 
   const parseSingleEntry = (entry, {subdoc, Artwork}) =>
@@ -820,6 +853,135 @@ export function parseArtwork({
   return transform;
 }
 
+export function parseContentEntriesFromSourceText(thingClass, sourceText, {subdoc}) {
+  function map(matchEntry) {
+    let artistText = null, artistReferences = null;
+
+    const artistTextNodes =
+      Array.from(
+        splitContentNodesAround(
+          parseContentNodes(matchEntry.artistText),
+          /\|/g));
+
+    const separatorIndices =
+      artistTextNodes
+        .filter(node => node.type === 'separator')
+        .map(node => artistTextNodes.indexOf(node));
+
+    if (empty(separatorIndices)) {
+      if (artistTextNodes.length === 1 && artistTextNodes[0].type === 'text') {
+        artistReferences = matchEntry.artistText;
+      } else {
+        artistText = matchEntry.artistText;
+      }
+    } else {
+      const firstSeparatorIndex =
+        separatorIndices.at(0);
+
+      const secondSeparatorIndex =
+        separatorIndices.at(1) ??
+        artistTextNodes.length;
+
+      artistReferences =
+        matchEntry.artistText.slice(
+          artistTextNodes.at(0).i,
+          artistTextNodes.at(firstSeparatorIndex - 1).iEnd);
+
+      artistText =
+        matchEntry.artistText.slice(
+          artistTextNodes.at(firstSeparatorIndex).iEnd,
+          artistTextNodes.at(secondSeparatorIndex - 1).iEnd);
+    }
+
+    if (artistReferences) {
+      artistReferences =
+        artistReferences
+          .split(',')
+          .map(ref => ref.trim());
+    }
+
+    return {
+      'Artists':
+        artistReferences,
+
+      'Artist Text':
+        artistText,
+
+      'Annotation':
+        matchEntry.annotation,
+
+      'Date':
+        matchEntry.date,
+
+      'Second Date':
+        matchEntry.secondDate,
+
+      'Date Kind':
+        matchEntry.dateKind,
+
+      'Access Date':
+        matchEntry.accessDate,
+
+      'Access Kind':
+        matchEntry.accessKind,
+
+      'Body':
+        matchEntry.body,
+    };
+  }
+
+  const documents =
+    matchContentEntries(sourceText)
+      .map(matchEntry =>
+        withEntries(
+          map(matchEntry),
+          entries => entries
+            .filter(([key, value]) =>
+              value !== undefined &&
+              value !== null)));
+
+  const subdocs =
+    documents.map(document =>
+      subdoc(thingClass, document, {bindInto: 'thing'}));
+
+  return subdocs;
+}
+
+export function parseContentEntries(thingClass, value, {subdoc}) {
+  if (typeof value === 'string') {
+    return parseContentEntriesFromSourceText(thingClass, value, {subdoc});
+  } else if (Array.isArray(value)) {
+    return value.map(doc => subdoc(thingClass, doc, {bindInto: 'thing'}));
+  } else {
+    return value;
+  }
+}
+
+export function parseCommentary(value, {subdoc, CommentaryEntry}) {
+  return parseContentEntries(CommentaryEntry, value, {subdoc});
+}
+
+export function parseCreditingSources(value, {subdoc, CreditingSourcesEntry}) {
+  return parseContentEntries(CreditingSourcesEntry, value, {subdoc});
+}
+
+export function parseReferencingSources(value, {subdoc, ReferencingSourcesEntry}) {
+  return parseContentEntries(ReferencingSourcesEntry, value, {subdoc});
+}
+
+export function parseLyrics(value, {subdoc, LyricsEntry}) {
+  if (
+    typeof value === 'string' &&
+    !multipleLyricsDetectionRegex.test(value)
+  ) {
+    const document = {'Body': value};
+
+    return [subdoc(LyricsEntry, document, {bindInto: 'thing'})];
+  }
+
+  return parseContentEntries(LyricsEntry, value, {subdoc});
+}
+
 // documentModes: Symbols indicating sets of behavior for loading and processing
 // data files.
 export const documentModes = {
@@ -849,6 +1011,12 @@ export const documentModes = {
   // array of processed documents (wiki objects).
   allInOne: Symbol('Document mode: allInOne'),
 
+  // allTogether: One or more documens, spread across any number of files.
+  // Expects files array (or function) and processDocument function.
+  // Calls save with an array of processed documents (wiki objects) - this is
+  // a flat array, *not* an array of the documents processed from *each* file.
+  allTogether: Symbol('Document mode: allTogether'),
+
   // oneDocumentTotal: Just a single document, represented in one file.
   // Expects file string (or function) and processDocument function. Calls
   // save with the single processed wiki document (data object).
@@ -895,7 +1063,7 @@ export const documentModes = {
 export function getAllDataSteps() {
   try {
     thingConstructors;
-  } catch (error) {
+  } catch {
     throw new Error(`Thing constructors aren't ready yet, can't get all data steps`);
   }
 
@@ -959,6 +1127,7 @@ export async function getFilesFromDataStep(dataStep, {dataPath}) {
       }
     }
 
+    case documentModes.allTogether:
     case documentModes.headerAndEntries:
     case documentModes.onePerFile: {
       if (!dataStep.files) {
@@ -1114,7 +1283,8 @@ export function processThingsFromDataStep(documents, dataStep) {
   const {documentMode} = dataStep;
 
   switch (documentMode) {
-    case documentModes.allInOne: {
+    case documentModes.allInOne:
+    case documentModes.allTogether: {
       const result = [];
       const aggregate = openAggregate({message: `Errors processing documents`});
 
@@ -1389,6 +1559,10 @@ export function saveThingsFromDataStep(thingLists, dataStep) {
       return dataStep.save(thing);
     }
 
+    case documentModes.allTogether: {
+      return dataStep.save(thingLists.flat());
+    }
+
     case documentModes.headerAndEntries:
     case documentModes.onePerFile: {
       return dataStep.save(thingLists);
@@ -1495,6 +1669,10 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
 
     ['artworkData', ['artworkData']],
 
+    ['commentaryData', [/* find */]],
+
+    ['creditingSourceData', [/* find */]],
+
     ['flashData', [
       'wikiInfo',
     ]],
@@ -1509,6 +1687,12 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
 
     ['homepageLayout.sections.rows', [/* find */]],
 
+    ['lyricsData', [/* find */]],
+
+    ['referencingSourceData', [/* find */]],
+
+    ['seriesData', [/* find */]],
+
     ['trackData', [
       'artworkData',
       'trackData',
@@ -1777,14 +1961,16 @@ export function flattenThingLayoutToDocumentOrder(layout) {
 }
 
 export function* splitDocumentsInYAMLSourceText(sourceText) {
-  const dividerRegex = /^-{3,}\n?/gm;
+  // Not multiline!
+  const dividerRegex = /(?:\r\n|\n|^)-{3,}(?:\r\n|\n|$)/g;
+
   let previousDivider = '';
 
   while (true) {
     const {lastIndex} = dividerRegex;
     const match = dividerRegex.exec(sourceText);
     if (match) {
-      const nextDivider = match[0].trim();
+      const nextDivider = match[0];
 
       yield {
         previousDivider,
@@ -1795,11 +1981,12 @@ export function* splitDocumentsInYAMLSourceText(sourceText) {
       previousDivider = nextDivider;
     } else {
       const nextDivider = '';
+      const lineBreak = previousDivider.match(/\r?\n/)?.[0] ?? '';
 
       yield {
         previousDivider,
         nextDivider,
-        text: sourceText.slice(lastIndex).replace(/(?<!\n)$/, '\n'),
+        text: sourceText.slice(lastIndex).replace(/(?<!\n)$/, lineBreak),
       };
 
       return;
@@ -1825,7 +2012,7 @@ export function recombineDocumentsIntoYAMLSourceText(documents) {
 
   for (const document of documents) {
     if (sourceText) {
-      sourceText += divider + '\n';
+      sourceText += divider;
     }
 
     sourceText += document.text;