UNPKG

akasharender

Version:

Rendering support for generating static HTML websites or EPUB eBooks

1,142 lines (1,006 loc) 43.5 kB
/** * * Copyright 2014-2025 David Herron * * This file is part of AkashaCMS (http://akashacms.com/). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import fsp from 'node:fs/promises'; import url from 'node:url'; import path from 'node:path'; import util from 'node:util'; import sharp from 'sharp'; import * as uuid from 'uuid'; const uuidv1 = uuid.v1; import * as render from './render.js'; import { Plugin } from './Plugin.js'; import relative from 'relative'; import hljs from 'highlight.js'; import mahabhuta from 'mahabhuta'; import mahaMetadata from 'mahabhuta/maha/metadata.js'; import mahaPartial from 'mahabhuta/maha/partial.js'; import Renderers from '@akashacms/renderers'; import {encode} from 'html-entities'; import { Configuration, CustomElement, Munger, PageProcessor, javaScriptItem } from './index.js'; const pluginName = "akashacms-builtin"; export class BuiltInPlugin extends Plugin { constructor() { super(pluginName); this.#resize_queue = []; } #config; #resize_queue; configure(config: Configuration, options) { this.#config = config; // this.config = config; this.akasha = config.akasha; this.options = options ? options : {}; this.options.config = config; if (typeof this.options.relativizeHeadLinks === 'undefined') { this.options.relativizeHeadLinks = true; } if (typeof this.options.relativizeScriptLinks === 'undefined') { this.options.relativizeScriptLinks = true; } if (typeof this.options.relativizeBodyLinks === 'undefined') { this.options.relativizeBodyLinks = true; } let moduleDirname = import.meta.dirname; // Need this as the place to store Nunjucks macros and templates config.addLayoutsDir(path.join(moduleDirname, '..', 'layouts')); config.addPartialsDir(path.join(moduleDirname, '..', 'partials')); // Do not need this here any longer because it is handled // in the Configuration constructor. The idea is to put // mahaPartial as the very first Mahafunc so that all // Partial's are handled before anything else. The issue causing // this change is the OpenGraphPromoteImages Mahafunc in // akashachs-base and processing any images brought in by partials. // Ensuring the partial tag is processed before OpenGraphPromoteImages // meant such images were properly promoted. // config.addMahabhuta(mahaPartial.mahabhutaArray({ // renderPartial: options.renderPartial // })); config.addMahabhuta(mahaMetadata.mahabhutaArray({ // Do not pass this through so that Mahabhuta will not // make absolute links to subdirectories // root_url: config.root_url // TODO how to configure this // sitemap_title: ....? })); config.addMahabhuta(mahabhutaArray(options, config, this.akasha, this)); const njk = this.config.findRendererName('.html.njk') as Renderers.NunjucksRenderer; njk.njkenv().addExtension('akstylesheets', new stylesheetsExtension(this.config, this, njk) ); njk.njkenv().addExtension('akheaderjs', new headerJavaScriptExtension(this.config, this, njk) ); njk.njkenv().addExtension('akfooterjs', new footerJavaScriptExtension(this.config, this, njk) ); // Verify that the extensions were installed for (const ext of [ 'akstylesheets', 'akheaderjs', 'akfooterjs' ]) { if (!njk.njkenv().hasExtension(ext)) { throw new Error(`Configure - NJK does not have extension - ${ext}`); } } // try { // njk.njkenv().addExtension('aknjktest', new testExtension()); // } catch (err) { // console.error(err.stack()); // } // if (!njk.njkenv().hasExtension('aknjktest')) { // console.error(`aknjktest extension not added?`); // } else { // console.log(`aknjktest exists`); // } } get config() { return this.#config; } // get resizequeue() { return this.#resize_queue; } get resizequeue() { return this.#resize_queue; } /** * Determine whether <link> tags in the <head> for local * URLs are relativized or absolutized. */ set relativizeHeadLinks(rel) { this.options.relativizeHeadLinks = rel; } /** * Determine whether <script> tags for local * URLs are relativized or absolutized. */ set relativizeScriptLinks(rel) { this.options.relativizeScriptLinks = rel; } /** * Determine whether <A> tags for local * URLs are relativized or absolutized. */ set relativizeBodyLinks(rel) { this.options.relativizeBodyLinks = rel; } doStylesheets(metadata) { return _doStylesheets(metadata, this.options, this.config); } doHeaderJavaScript(metadata) { return _doHeaderJavaScript(metadata, this.options, this.config); } doFooterJavaScript(metadata) { return _doFooterJavaScript(metadata, this.options, this.config); } addImageToResize(src: string, resizewidth: number, resizeto: string, docPath: string) { // console.log(`addImageToResize ${src} resizewidth ${resizewidth} resizeto ${resizeto}`) this.#resize_queue.push({ src, resizewidth, resizeto, docPath }); } async onSiteRendered(config) { const documents = this.akasha.filecache.documentsCache; // await documents.isReady(); const assets = this.akasha.filecache.assetsCache; // await assets.isReady(); while (Array.isArray(this.#resize_queue) && this.#resize_queue.length > 0) { let toresize = this.#resize_queue.pop(); let img2resize; if (!path.isAbsolute(toresize.src)) { img2resize = path.normalize(path.join( path.dirname(toresize.docPath), toresize.src )); } else { img2resize = toresize.src; } let srcfile = undefined; let found = await assets.find(img2resize); if (found) { srcfile = found.fspath; } else { found = await documents.find(img2resize); srcfile = found ? found.fspath : undefined; } if (!srcfile) throw new Error(`akashacms-builtin: Did not find source file for image to resize ${img2resize}`); try { let img = await sharp(srcfile); let resized = await img.resize(Number.parseInt(toresize.resizewidth)); // We need to compute the correct destination path // for the resized image let imgtoresize = toresize.resizeto ? toresize.resizeto : img2resize; let resizedest; if (path.isAbsolute(imgtoresize)) { resizedest = path.join(config.renderDestination, imgtoresize); } else { // This is for relative image paths, hence it needs to be // relative to the docPath resizedest = path.join( config.renderDestination, path.dirname(toresize.docPath), imgtoresize); } // Make sure the destination directory exists await fsp.mkdir(path.dirname(resizedest), { recursive: true }); await resized.toFile(resizedest); } catch (e) { throw new Error(`built-in: Image resize failed for ${srcfile} (toresize ${util.inspect(toresize)} found ${util.inspect(found)}) because ${e}`); } } } } export const mahabhutaArray = function( options, config?: Configuration, akasha?: any, plugin?: Plugin ) { let ret = new mahabhuta.MahafuncArray(pluginName, options); ret.addMahafunc(new StylesheetsElement(config, akasha, plugin)); ret.addMahafunc(new HeaderJavaScript(config, akasha, plugin)); ret.addMahafunc(new FooterJavaScript(config, akasha, plugin)); ret.addMahafunc(new HeadLinkRelativizer(config, akasha, plugin)); ret.addMahafunc(new ScriptRelativizer(config, akasha, plugin)); ret.addMahafunc(new InsertTeaser(config, akasha, plugin)); ret.addMahafunc(new CodeEmbed(config, akasha, plugin)); ret.addMahafunc(new AkBodyClassAdd(config, akasha, plugin)); ret.addMahafunc(new FigureImage(config, akasha, plugin)); ret.addMahafunc(new img2figureImage(config, akasha, plugin)); ret.addMahafunc(new ImageRewriter(config, akasha, plugin)); ret.addMahafunc(new ShowContent(config, akasha, plugin)); ret.addMahafunc(new SelectElements(config, akasha, plugin)); ret.addMahafunc(new AnchorCleanup(config, akasha, plugin)); ret.addFinalMahafunc(new MungedAttrRemover(config, akasha, plugin)); return ret; }; function _doStylesheets(metadata, options, config: Configuration) { // console.log(`_doStylesheets ${util.inspect(metadata)}`); var scripts; if (typeof metadata.headerStylesheetsAdd !== "undefined") { scripts = config.scripts.stylesheets.concat(metadata.headerStylesheetsAdd); } else { scripts = config.scripts ? config.scripts.stylesheets : undefined; } // console.log(`ak-stylesheets ${metadata.document.path} ${util.inspect(metadata.headerStylesheetsAdd)} ${util.inspect(config.scripts)} ${util.inspect(scripts)}`); if (!options) throw new Error('_doStylesheets no options'); if (!config) throw new Error('_doStylesheets no config'); var ret = ''; if (typeof scripts !== 'undefined') { for (var style of scripts) { let stylehref = style.href; let uHref = new URL(style.href, 'http://example.com'); // console.log(`_doStylesheets process ${stylehref}`); if (uHref.origin === 'http://example.com') { // This is a local URL // Only relativize if desired // The bit with 'http://example.com' means there // won't be an exception thrown for a local URL. // But, in such a case, uHref.pathname would // start with a slash. Therefore, to correctly // determine if this URL is absolute we must check // with the original URL string, which is in // the stylehref variable. if (options.relativizeHeadLinks && path.isAbsolute(stylehref)) { /* if (!metadata) { console.log(`_doStylesheets NO METADATA`); } else if (!metadata.document) { console.log(`_doStylesheets NO METADATA DOCUMENT`); } else if (!metadata.document.renderTo) { console.log(`_doStylesheets NO METADATA DOCUMENT RENDERTO`); } else { console.log(`_doStylesheets relative(/${metadata.document.renderTo}, ${stylehref}) = ${relative('/'+metadata.document.renderTo, stylehref)}`) } */ let newHref = relative(`/${metadata.document.renderTo}`, stylehref); // console.log(`_doStylesheets absolute stylehref ${stylehref} in ${util.inspect(metadata.document)} rewrote to ${newHref}`); stylehref = newHref; } } const doStyleMedia = (media) => { if (media) { return `media="${encode(media)}"` } else { return ''; } }; let ht = `<link rel="stylesheet" type="text/css" href="${encode(stylehref)}" ${doStyleMedia(style.media)}/>` ret += ht; // The issue with this and other instances // is that this tended to result in // // <html><body><link..></body></html> // // When it needed to just be the <link> tag. // In other words, it tried to create an entire // HTML document. While there was a way around // this - $('selector').prop('outerHTML') // This also seemed to be an overhead // we can avoid. // // The pattern is to use Template Strings while // being careful to encode values safely for use // in an attribute. The "encode" function does // the encoding. // // See https://github.com/akashacms/akasharender/issues/49 // let $ = mahabhuta.parse('<link rel="stylesheet" type="text/css" href=""/>'); // $('link').attr('href', stylehref); // if (style.media) { // $('link').attr('media', style.media); // } // ret += $.html(); } // console.log(`_doStylesheets ${ret}`); } return ret; } function _doJavaScripts( metadata, scripts: javaScriptItem[], options, config: Configuration ) { var ret = ''; if (!scripts) return ret; if (!options) throw new Error('_doJavaScripts no options'); if (!config) throw new Error('_doJavaScripts no config'); for (var script of scripts) { if (!script.href && !script.script) { throw new Error(`Must specify either href or script in ${util.inspect(script)}`); } if (!script.script) script.script = ''; const doType = (lang) => { if (lang) { return `type="${encode(lang)}"`; } else { return ''; } } const doHref = (href) => { if (href) { let scripthref = href; let uHref = new URL(href, 'http://example.com'); if (uHref.origin === 'http://example.com') { // This is a local URL // Only relativize if desired if (options.relativizeScriptLinks && path.isAbsolute(scripthref)) { let newHref = relative(`/${metadata.document.renderTo}`, scripthref); // console.log(`_doJavaScripts absolute scripthref ${scripthref} in ${util.inspect(metadata.document)} rewrote to ${newHref}`); scripthref = newHref; } } return `src="${encode(scripthref)}"`; } else { return ''; } }; let ht = `<script ${doType(script.lang)} ${doHref(script.href)}>${script.script}</script>`; ret += ht; } return ret; } function _doHeaderJavaScript(metadata, options, config: Configuration) { var scripts; if (typeof metadata.headerJavaScriptAddTop !== "undefined") { scripts = config.scripts.javaScriptTop.concat(metadata.headerJavaScriptAddTop); } else { scripts = config.scripts ? config.scripts.javaScriptTop : undefined; } // console.log(`_doHeaderJavaScript ${util.inspect(scripts)}`); // console.log(`_doHeaderJavaScript ${util.inspect(config.scripts)}`); return _doJavaScripts(metadata, scripts, options, config); } function _doFooterJavaScript(metadata, options, config: Configuration) { var scripts; if (typeof metadata.headerJavaScriptAddBottom !== "undefined") { scripts = config.scripts.javaScriptBottom.concat(metadata.headerJavaScriptAddBottom); } else { scripts = config.scripts ? config.scripts.javaScriptBottom : undefined; } return _doJavaScripts(metadata, scripts, options, config); } class StylesheetsElement extends CustomElement { get elementName() { return "ak-stylesheets"; } async process($element, metadata, setDirty: Function, done?: Function) { let ret = _doStylesheets(metadata, this.array.options, this.config); // console.log(`StylesheetsElement `, ret); return ret; } } class HeaderJavaScript extends CustomElement { get elementName() { return "ak-headerJavaScript"; } async process($element, metadata, setDirty: Function, done?: Function) { let ret = _doHeaderJavaScript(metadata, this.array.options, this.config); // console.log(`HeaderJavaScript `, ret); return ret; } } class FooterJavaScript extends CustomElement { get elementName() { return "ak-footerJavaScript"; } async process($element, metadata, dirty) { return _doFooterJavaScript(metadata, this.array.options, this.config); } } class HeadLinkRelativizer extends Munger { get selector() { return "html head link"; } get elementName() { return "html head link"; } async process($, $link, metadata, dirty): Promise<string> { // Only relativize if desired if (!this.array.options.relativizeHeadLinks) return; let href = $link.attr('href'); let uHref = new URL(href, 'http://example.com'); if (uHref.origin === 'http://example.com') { // It's a local link if (path.isAbsolute(href)) { // It's an absolute local link let newHref = relative(`/${metadata.document.renderTo}`, href); $link.attr('href', newHref); } } } } class ScriptRelativizer extends Munger { get selector() { return "script"; } get elementName() { return "script"; } async process($, $link, metadata, dirty): Promise<string> { // Only relativize if desired if (!this.array.options.relativizeScriptLinks) return; let href = $link.attr('src'); if (href) { // There is a link let uHref = new URL(href, 'http://example.com'); if (uHref.origin === 'http://example.com') { // It's a local link if (path.isAbsolute(href)) { // It's an absolute local link let newHref = relative(`/${metadata.document.renderTo}`, href); $link.attr('src', newHref); } } } } } class InsertTeaser extends CustomElement { get elementName() { return "ak-teaser"; } async process($element, metadata, dirty) { try { return this.akasha.partial(this.config, "ak_teaser.html.njk", { teaser: typeof metadata["ak-teaser"] !== "undefined" ? metadata["ak-teaser"] : metadata.teaser }); } catch (e) { console.error(`InsertTeaser caught error `, e); throw e; } } } class AkBodyClassAdd extends PageProcessor { async process($, metadata, dirty): Promise<string> { if (typeof metadata.akBodyClassAdd !== 'undefined' && metadata.akBodyClassAdd != '' && $('html body').get(0)) { return new Promise((resolve, reject) => { if (!$('html body').hasClass(metadata.akBodyClassAdd)) { $('html body').addClass(metadata.akBodyClassAdd); } resolve(undefined); }); } else return Promise.resolve(''); } } class CodeEmbed extends CustomElement { get elementName() { return "code-embed"; } async process($element, metadata, dirty) { const fn = $element.attr('file-name'); const lang = $element.attr('lang'); const id = $element.attr('id'); if (!fn || fn === '') { throw new Error(`code-embed must have file-name argument, got ${fn}`); } let txtpath; if (path.isAbsolute(fn)) { txtpath = fn; } else { txtpath = path.join(path.dirname(metadata.document.renderTo), fn); } const documents = this.akasha.filecache.documentsCache; const found = await documents.find(txtpath); if (!found) { throw new Error(`code-embed file-name ${fn} does not refer to usable file`); } const txt = await fsp.readFile(found.fspath, 'utf8'); const doLang = (lang) => { if (lang) { return `class="hljs ${encode(lang)}"`; } else { return 'class="hljs"'; } }; const doID = (id) => { if (id) { return `id="${encode(id)}"`; } else { return ''; } } const doCode = (lang, code) => { if (lang && lang != '') { return hljs.highlight(code, { language: lang }).value; } else { return hljs.highlightAuto(code).value; } } let ret = `<pre ${doID(id)}><code ${doLang(lang)}>${doCode(lang, txt)}</code></pre>`; return ret; // let $ = mahabhuta.parse(`<pre><code></code></pre>`); // if (lang && lang !== '') { // $('code').addClass(lang); // } // $('code').addClass('hljs'); // if (id && id !== '') { // $('pre').attr('id', id); // } // if (lang && lang !== '') { // $('code').append(hljs.highlight(txt, { // language: lang // }).value); // } else { // $('code').append(hljs.highlightAuto(txt).value); // } // return $.html(); } } class FigureImage extends CustomElement { get elementName() { return "fig-img"; } async process($element, metadata, dirty) { var template = $element.attr('template'); if (!template) template = 'ak_figimg.html.njk'; const href = $element.attr('href'); if (!href) throw new Error('fig-img must receive an href'); const clazz = $element.attr('class'); const id = $element.attr('id'); const caption = $element.html(); const width = $element.attr('width'); const style = $element.attr('style'); const dest = $element.attr('dest'); return this.akasha.partial( this.config, template, { href, clazz, id, caption, width, style, dest }); } } class img2figureImage extends CustomElement { get elementName() { return 'html body img[figure]'; } async process($element, metadata, dirty, done) { // console.log($element); const template = $element.attr('template') ? $element.attr('template') : "ak_figimg.html.njk"; const id = $element.attr('id'); const clazz = $element.attr('class'); const style = $element.attr('style'); const width = $element.attr('width'); const src = $element.attr('src'); const dest = $element.attr('dest'); const resizewidth = $element.attr('resize-width'); const resizeto = $element.attr('resize-to'); const content = $element.attr('caption') ? $element.attr('caption') : ""; return this.akasha.partial( this.config, template, { id, clazz, style, width, href: src, dest, resizewidth, resizeto, caption: content }); } } class ImageRewriter extends Munger { get selector() { return "html body img"; } get elementName() { return "html body img"; } async process($, $link, metadata, dirty) { // console.log($element); // We only do rewrites for local images let src = $link.attr('src'); // For local URLs, this new URL call will // make uSrc.origin === http://example.com // Hence, if some other domain appears // then we konw it's not a local image. const uSrc = new URL(src, 'http://example.com'); if (uSrc.origin !== 'http://example.com') { return "ok"; } // Are we asked to resize the image? const resizewidth = $link.attr('resize-width'); const resizeto = $link.attr('resize-to'); if (resizewidth) { // Add to a queue that is run at the end this.config.plugin(pluginName) .addImageToResize(src, resizewidth, resizeto, metadata.document.renderTo); if (resizeto) { $link.attr('src', resizeto); src = resizeto; } // These are no longer needed $link.removeAttr('resize-width'); $link.removeAttr('resize-to'); } // The idea here is for every local image src to be a relative URL if (path.isAbsolute(src)) { let newSrc = relative(`/${metadata.document.renderTo}`, src); $link.attr('src', newSrc); // console.log(`ImageRewriter absolute image path ${src} rewrote to ${newSrc}`); src = newSrc; } return "ok"; } } class ShowContent extends CustomElement { get elementName() { return "show-content"; } async process($element, metadata, dirty) { var template = $element.attr('template'); if (!template) template = 'ak_show-content.html.njk'; let href = $element.attr('href'); if (!href) return Promise.reject(new Error('show-content must receive an href')); const clazz = $element.attr('class'); const id = $element.attr('id'); const caption = $element.html(); const width = $element.attr('width'); const style = $element.attr('style'); const dest = $element.attr('dest'); const contentImage = $element.attr('content-image'); let doc2read; if (! href.startsWith('/')) { let dir = path.dirname(metadata.document.path); doc2read = path.join('/', dir, href); } else { doc2read = href; } // console.log(`ShowContent ${util.inspect(metadata.document)} ${doc2read}`); const documents = this.akasha.filecache.documentsCache; const doc = await documents.find(doc2read); const data = { href, clazz, id, caption, width, style, dest, contentImage, document: doc }; let ret = await this.akasha.partial( this.config, template, data); // console.log(`ShowContent ${href} ${util.inspect(data)} ==> ${ret}`); return ret; } } /* This was moved into Mahabhuta class Partial extends mahabhuta.CustomElement { get elementName() { return "partial"; } process($element, metadata, dirty) { // We default to making partial set the dirty flag. But a user // of the partial tag can choose to tell us it isn't dirty. // For example, if the partial only substitutes normal tags // there's no need to do the dirty thing. var dothedirtything = $element.attr('dirty'); if (!dothedirtything || dothedirtything.match(/true/i)) { dirty(); } var fname = $element.attr("file-name"); var txt = $element.html(); var d = {}; for (var mprop in metadata) { d[mprop] = metadata[mprop]; } var data = $element.data(); for (var dprop in data) { d[dprop] = data[dprop]; } d["partialBody"] = txt; log('partial tag fname='+ fname +' attrs '+ util.inspect(data)); return render.partial(this.config, fname, d) .then(html => { return html; }) .catch(err => { error(new Error("FAIL partial file-name="+ fname +" because "+ err)); throw new Error("FAIL partial file-name="+ fname +" because "+ err); }); } } module.exports.mahabhuta.addMahafunc(new Partial()); */ // // <select-elements class=".." id=".." count="N"> // <element></element> // <element></element> // </select-elements> // class SelectElements extends Munger { get selector() { return "select-elements"; } get elementName() { return "select-elements"; } async process($, $link, metadata, dirty): Promise<string> { let count = $link.attr('count') ? Number.parseInt($link.attr('count')) : 1; const clazz = $link.attr('class'); const id = $link.attr('id'); const tn = $link.attr('tag-name') ? $link.attr('tag-name') : 'div'; const children = $link.children(); const selected = []; for (; count >= 1 && children.length >= 1; count--) { // console.log(`SelectElements `, children.length); const _n = Math.floor(Math.random() * children.length); // console.log(_n); const chosen = $(children[_n]).html(); selected.push(chosen); // console.log(`SelectElements `, chosen); delete children[_n]; } const _uuid = uuidv1(); $link.replaceWith(`<${tn} id='${_uuid}'></${tn}>`); const $newItem = $(`${tn}#${_uuid}`); if (id) $newItem.attr('id', id); else $newItem.removeAttr('id'); if (clazz) $newItem.addClass(clazz); for (let chosen of selected) { $newItem.append(chosen); } return ''; } } class AnchorCleanup extends Munger { get selector() { return "html body a[munged!='yes']"; } get elementName() { return "html body a[munged!='yes']"; } async process($, $link, metadata, dirty) { var href = $link.attr('href'); var linktext = $link.text(); const documents = this.akasha.filecache.documentsCache; // await documents.isReady(); const assets = this.akasha.filecache.assetsCache; // await assets.isReady(); // console.log(`AnchorCleanup ${href} ${linktext}`); if (href && href !== '#') { const uHref = new URL(href, 'http://example.com'); if (uHref.origin !== 'http://example.com') return "ok"; if (!uHref.pathname) return "ok"; // console.log(`AnchorCleanup is local ${href} ${linktext} uHref ${uHref.pathname}`); /* if (metadata.document.path === 'index.html.md') { console.log(`AnchorCleanup metadata.document.path ${metadata.document.path} href ${href} uHref.pathname ${uHref.pathname} this.config.root_url ${this.config.root_url}`); console.log($.html()); } */ // let startTime = new Date(); // We have determined this is a local href. // For reference we need the absolute pathname of the href within // the project. For example to retrieve the title when we're filling // in for an empty <a> we need the absolute pathname. // Mark this link as having been processed. // The purpose is if Mahabhuta runs multiple passes, // to not process the link multiple times. // Before adding this - we saw this Munger take as much // as 800ms to execute, for EVERY pass made by Mahabhuta. // // Adding this attribute, and checking for it in the selector, // means we only process the link once. $link.attr('munged', 'yes'); let absolutePath; if (!path.isAbsolute(href)) { absolutePath = path.join(path.dirname(metadata.document.path), href); // console.log(`AnchorCleanup href ${href} uHref.pathname ${uHref.pathname} not absolute, absolutePath ${absolutePath}`); } else { absolutePath = href; // console.log(`AnchorCleanup href ${href} uHref.pathname ${uHref.pathname} absolute, absolutePath ${absolutePath}`); } // The idea for this section is to ensure all local href's are // for a relative path rather than an absolute path // Hence we use the relative module to compute the relative path // // Example: // // AnchorCleanup de-absolute href /index.html in { // basedir: '/Volumes/Extra/akasharender/akasharender/test/documents', // relpath: 'hier/dir1/dir2/nested-anchor.html.md', // relrender: 'hier/dir1/dir2/nested-anchor.html', // path: 'hier/dir1/dir2/nested-anchor.html.md', // renderTo: 'hier/dir1/dir2/nested-anchor.html' // } to ../../../index.html // // Only relativize if desired if (this.array.options.relativizeBodyLinks && path.isAbsolute(href)) { let newHref = relative(`/${metadata.document.renderTo}`, href); $link.attr('href', newHref); // console.log(`AnchorCleanup de-absolute href ${href} in ${util.inspect(metadata.document)} to ${newHref}`); } // Look to see if it's an asset file let foundAsset; try { foundAsset = await assets.find(absolutePath); } catch (e) { foundAsset = undefined; } if (foundAsset) { // console.log(`AnchorCleanup is asset ${absolutePath}`); return "ok"; } // console.log(`AnchorCleanup ${metadata.document.path} ${href} findAsset ${(new Date() - startTime) / 1000} seconds`); // Ask plugins if the href is okay if (this.config.askPluginsLegitLocalHref(absolutePath)) { // console.log(`AnchorCleanup is legit local href ${absolutePath}`); return "ok"; } // If this link has a body, then don't modify it if ((linktext && linktext.length > 0 && linktext !== absolutePath) || ($link.children().length > 0)) { // console.log(`AnchorCleanup skipping ${absolutePath} w/ ${util.inspect(linktext)} children= ${$link.children()}`); return "ok"; } // Does it exist in documents dir? if (absolutePath === '/') { absolutePath = '/index.html'; } let found = await documents.find(absolutePath); // console.log(`AnchorCleanup findRendersTo ${absolutePath} ${util.inspect(found)}`); if (!found) { console.log(`WARNING: Did not find ${href} in ${util.inspect(this.config.documentDirs)} in ${metadata.document.path} absolutePath ${absolutePath}`); return "ok"; } // console.log(`AnchorCleanup ${metadata.document.path} ${href} findRendersTo ${(new Date() - startTime) / 1000} seconds`); // If this is a directory, there might be /path/to/index.html so we try for that. // The problem is that this.config.findRendererPath would fail on just /path/to but succeed // on /path/to/index.html if (found.isDirectory) { found = await documents.find(path.join(absolutePath, "index.html")); if (!found) { throw new Error(`Did not find ${href} in ${util.inspect(this.config.documentDirs)} in ${metadata.document.path}`); } } // Otherwise look into filling emptiness with title let docmeta = found.docMetadata; // Automatically add a title= attribute if (!$link.attr('title') && docmeta && docmeta.title) { $link.attr('title', docmeta.title); } if (docmeta && docmeta.title) { // console.log(`AnchorCleanup changed link text ${href} to ${docmeta.title}`); $link.text(docmeta.title); } else { // console.log(`AnchorCleanup changed link text ${href} to ${href}`); $link.text(href); } /* var renderer = this.config.findRendererPath(found.vpath); // console.log(`AnchorCleanup ${metadata.document.path} ${href} findRendererPath ${(new Date() - startTime) / 1000} seconds`); if (renderer && renderer.metadata) { let docmeta = found.docMetadata; /* try { var docmeta = await renderer.metadata(found.foundDir, found.foundPathWithinDir); } catch(err) { throw new Error(`Could not retrieve document metadata for ${found.foundDir} ${found.foundPathWithinDir} because ${err}`); } *--/ // Automatically add a title= attribute if (!$link.attr('title') && docmeta && docmeta.title) { $link.attr('title', docmeta.title); } if (docmeta && docmeta.title) { $link.text(docmeta.title); } // console.log(`AnchorCleanup finished`); // console.log(`AnchorCleanup ${metadata.document.path} ${href} DONE ${(new Date() - startTime) / 1000} seconds`); return "ok"; } else { // Don't bother throwing an error. Just fill it in with // something. $link.text(href); // throw new Error(`Could not fill in empty 'a' element in ${metadata.document.path} with href ${href}`); } */ } else { return "ok"; } } } //////////////// MAHAFUNCS FOR FINAL PASS /** * Removes the <code>munged=yes</code> attribute that is added * by <code>AnchorCleanup</code>. */ class MungedAttrRemover extends Munger { get selector() { return 'html body a[munged]'; } get elementName() { return 'html body a[munged]'; } async process($, $element, metadata, setDirty: Function, done?: Function): Promise<string> { // console.log($element); $element.removeAttr('munged'); return ''; } } ////////////// Nunjucks Extensions // From https://github.com/softonic/nunjucks-include-with/tree/master class stylesheetsExtension { tags; config; plugin; njkRenderer; constructor(config, plugin, njkRenderer) { this.tags = [ 'akstylesheets' ]; this.config = config; this.plugin = plugin; this.njkRenderer = njkRenderer; // console.log(`stylesheetsExtension ${util.inspect(this.tags)} ${util.inspect(this.config)} ${util.inspect(this.plugin)}`); } parse(parser, nodes, lexer) { // console.log(`in stylesheetsExtension - parse`); try { // get the tag token var tok = parser.nextToken(); // parse the args and move after the block end. passing true // as the second arg is required if there are no parentheses var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); // parse the body and possibly the error block, which is optional var body = parser.parseUntilBlocks('endakstylesheets'); parser.advanceAfterBlockEnd(); // See above for notes about CallExtension return new nodes.CallExtension(this, 'run', args, [body]); } catch (err) { console.error(`stylesheetsExtension `, err.stack); } } run(context, args, body) { // console.log(`stylesheetsExtension ${util.inspect(context)}`); return this.plugin.doStylesheets(context.ctx); }; } class headerJavaScriptExtension { tags; config; plugin; njkRenderer; constructor(config, plugin, njkRenderer) { this.tags = [ 'akheaderjs' ]; this.config = config; this.plugin = plugin; this.njkRenderer = njkRenderer; // console.log(`headerJavaScriptExtension ${util.inspect(this.tags)} ${util.inspect(this.config)} ${util.inspect(this.plugin)}`); } parse(parser, nodes, lexer) { // console.log(`in headerJavaScriptExtension - parse`); try { var tok = parser.nextToken(); var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); var body = parser.parseUntilBlocks('endakheaderjs'); parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, 'run', args, [body]); } catch (err) { console.error(`headerJavaScriptExtension `, err.stack); } } run(context, args, body) { // console.log(`headerJavaScriptExtension ${util.inspect(context)}`); return this.plugin.doHeaderJavaScript(context.ctx); }; } class footerJavaScriptExtension { tags; config; plugin; njkRenderer; constructor(config, plugin, njkRenderer) { this.tags = [ 'akfooterjs' ]; this.config = config; this.plugin = plugin; this.njkRenderer = njkRenderer; // console.log(`footerJavaScriptExtension ${util.inspect(this.tags)} ${util.inspect(this.config)} ${util.inspect(this.plugin)}`); } parse(parser, nodes, lexer) { // console.log(`in footerJavaScriptExtension - parse`); try { var tok = parser.nextToken(); var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); var body = parser.parseUntilBlocks('endakfooterjs'); parser.advanceAfterBlockEnd(); return new nodes.CallExtension(this, 'run', args, [body]); } catch (err) { console.error(`footerJavaScriptExtension `, err.stack); } } run(context, args, body) { // console.log(`footerJavaScriptExtension ${util.inspect(context)}`); return this.plugin.doFooterJavaScript(context.ctx); }; } function testExtension() { this.tags = [ 'aknjktest' ]; this.parse = function(parser, nodes, lexer) { console.log(`in testExtension - parse`); try { // get the tag token var tok = parser.nextToken(); // parse the args and move after the block end. passing true // as the second arg is required if there are no parentheses var args = parser.parseSignature(null, true); parser.advanceAfterBlockEnd(tok.value); // parse the body and possibly the error block, which is optional var body = parser.parseUntilBlocks('error', 'endaknjktest'); var errorBody = null; if(parser.skipSymbol('error')) { parser.skip(lexer.TOKEN_BLOCK_END); errorBody = parser.parseUntilBlocks('endaknjktest'); } parser.advanceAfterBlockEnd(); // See above for notes about CallExtension return new nodes.CallExtension(this, 'run', args, [body, errorBody]); } catch (err) { console.error(`testExtionsion `, err.stack); } }; this.run = function(context, url, body, errorBody) { console.log(`aknjktest ${util.inspect(context)} ${util.inspect(url)} ${util.inspect(body)} ${util.inspect(errorBody)}`); }; }