« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content/dependencies/transformContent.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/dependencies/transformContent.js')
-rw-r--r--src/content/dependencies/transformContent.js168
1 files changed, 116 insertions, 52 deletions
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index faae35a..0904cde 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -37,6 +37,7 @@ export default {
         .map(description => description.link)
         .filter(Boolean)),
     'image',
+    'linkExternal',
   ],
 
   extraDependencies: ['html', 'language', 'to', 'wikiData'],
@@ -114,7 +115,7 @@ export default {
 
             data.hash = enteredHash ?? null;
 
-            return {i: node.i, iEnd: node.iEnd, type: 'link', data};
+            return {i: node.i, iEnd: node.iEnd, type: 'internal-link', data};
           }
 
           // This will be another {type: 'tag'} node which gets processed in
@@ -140,10 +141,15 @@ export default {
         sprawl.nodes
           .map(node => {
             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'};
+              // Replace internal link nodes with a stub. It'll be replaced
+              // (by position) with an item from relations.
+              //
+              // TODO: This should be where label and hash get passed through,
+              // rather than in relations... (in which case there's no need to
+              // handle it specially here, and we can really just return
+              // data.nodes = sprawl.nodes)
+              case 'internal-link':
+                return {type: 'internal-link'};
 
               // Other nodes will get processed in generate.
               default:
@@ -167,9 +173,9 @@ export default {
           : getPlaceholder(node, content));
 
     return {
-      links:
+      internalLinks:
         nodes
-          .filter(({type}) => type === 'link')
+          .filter(({type}) => type === 'internal-link')
           .map(node => {
             const {link, thing, value} = node.data;
 
@@ -182,6 +188,15 @@ export default {
             }
           }),
 
+      externalLinks:
+        nodes
+          .filter(({type}) => type === 'external-link')
+          .map(node => {
+            const {href} = node.data;
+
+            return relation('linkExternal', href);
+          }),
+
       images:
         nodes
           .filter(({type}) => type === 'image')
@@ -201,6 +216,11 @@ export default {
       default: false,
     },
 
+    indicateExternalLinks: {
+      type: 'boolean',
+      default: true,
+    },
+
     thumb: {
       validate: v => v.is('small', 'medium', 'large'),
       default: 'large',
@@ -208,12 +228,10 @@ export default {
   },
 
   generate(data, relations, slots, {html, language, to}) {
-    let linkIndex = 0;
     let imageIndex = 0;
+    let internalLinkIndex = 0;
+    let externalLinkIndex = 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 => {
         switch (node.type) {
@@ -237,57 +255,83 @@ export default {
             } = node;
 
             if (node.inline) {
+              let content =
+                html.tag('img',
+                  src && {src},
+                  width && {width},
+                  height && {height},
+                  style && {style},
+
+                  pixelate &&
+                    {class: 'pixelate'});
+
+              if (link) {
+                content =
+                  html.tag('a',
+                    {href: link},
+                    {target: '_blank'},
+
+                    {title:
+                      language.$('misc.external.opensInNewTab', {
+                        link:
+                          language.formatExternalLink(link, {
+                            style: 'platform',
+                          }),
+
+                        annotation:
+                          language.$('misc.external.opensInNewTab.annotation'),
+                      }).toString()},
+
+                    content);
+              }
+
               return {
-                type: 'image',
+                type: 'processed-image',
                 inline: true,
-                data:
-                  html.tag('img',
-                    src && {src},
-                    width && {width},
-                    height && {height},
-                    style && {style},
-
-                    pixelate &&
-                      {class: 'pixelate'}),
+                data: content,
               };
             }
 
             const image = relations.images[imageIndex++];
 
+            image.setSlots({
+              src,
+
+              link: link ?? true,
+              warnings: warnings ?? null,
+              thumb: slots.thumb,
+            });
+
+            if (width || height) {
+              image.setSlot('dimensions', [width ?? null, height ?? null]);
+            }
+
+            image.setSlot('attributes', [
+              {class: 'content-image'},
+
+              pixelate &&
+                {class: 'pixelate'},
+            ]);
+
             return {
-              type: 'image',
+              type: 'processed-image',
               inline: false,
               data:
                 html.tag('div', {class: 'content-image-container'},
                   align === 'center' &&
                     {class: 'align-center'},
 
-                  image.slots({
-                    src,
-
-                    link: link ?? true,
-                    width: width ?? null,
-                    height: height ?? null,
-                    warnings: warnings ?? null,
-                    thumb: slots.thumb,
-
-                    attributes: [
-                      {class: 'content-image'},
-
-                      pixelate &&
-                        {class: 'pixelate'},
-                    ],
-                  })),
+                  image),
             };
           }
 
-          case 'link': {
-            const linkNode = relations.links[linkIndex++];
-            if (linkNode.type === 'text') {
-              return {type: 'text', data: linkNode.data};
+          case 'internal-link': {
+            const nodeFromRelations = relations.internalLinks[internalLinkIndex++];
+            if (nodeFromRelations.type === 'text') {
+              return {type: 'text', data: nodeFromRelations.data};
             }
 
-            const {link, label, hash} = linkNode;
+            const {link, label, hash} = nodeFromRelations;
 
             // These are removed from the typical combined slots({})-style
             // because we don't want to override slots that were already set
@@ -322,7 +366,27 @@ export default {
               link.setSlot('tooltipStyle', 'none');
             }
 
-            return {type: 'link', data: link};
+            return {type: 'processed-internal-link', data: link};
+          }
+
+          case 'external-link': {
+            const {label} = node.data;
+            const externalLink = relations.externalLinks[externalLinkIndex++];
+
+            externalLink.setSlots({
+              content: label,
+              fromContent: true,
+            });
+
+            if (slots.indicateExternalLinks) {
+              externalLink.setSlots({
+                indicateExternal: true,
+                tab: 'separate',
+                style: 'platform',
+              });
+            }
+
+            return {type: 'processed-external-link', data: externalLink};
           }
 
           case 'tag': {
@@ -358,7 +422,10 @@ export default {
     // access to its slots.
 
     if (slots.mode === 'single-link') {
-      const link = contentFromNodes.find(node => node.type === 'link');
+      const link =
+        contentFromNodes.find(node =>
+          node.type === 'processed-internal-link' ||
+          node.type === 'processed-external-link');
 
       if (!link) {
         return html.blank();
@@ -385,13 +452,10 @@ export default {
             return getTextNodeContents(node, index);
           }
 
-          const attributes = html.attributes({
-            class: 'INSERT-NON-TEXT',
-            'data-type': node.type,
-          });
+          let attributes = `class="INSERT-NON-TEXT" data-type="${node.type}"`;
 
-          if (node.type === 'image') {
-            attributes.set('data-inline', node.inline);
+          if (node.type === 'processed-image' && node.inline) {
+            attributes += ` data-inline`;
           }
 
           return `<span ${attributes}>${index}</span>`;
@@ -426,7 +490,7 @@ export default {
         // the surrounding <p> tag that marked generates. The HTML parser
         // treats a <div> that starts inside a <p> as a Crocker-class
         // misgiving, and will treat you very badly if you feed it that.
-        if (attributes.get('data-type') === 'image') {
+        if (attributes.get('data-type') === 'processed-image') {
           if (!attributes.get('data-inline')) {
             tags[tags.length - 1] = tags[tags.length - 1].replace(/<p>$/, '');
             deleteParagraph = true;