import fs from "node:fs";
import path from "node:path";
import * as util from "./util.js";
import { CONTENT_ROOT } from "../../../libs/env/index.js";
import { KumaThis } from "../environment.js";
import { ONLY_AVAILABLE_IN_ENGLISH } from "../../../libs/l10n/l10n.js";
import { htmlEscape } from "./util.js";
const DUMMY_BASE_URL = "https://example.com";
const _warned = new Map();
// The purpose of this function is to make sure `console.warn` is only called once
// per 'macro' per 'href'.
// There are some macros that use `smartLink` within themselves and these macros
// might be buggy and that's not the fault of the person using the wrapping
// macro. And because these might be used over and over and over we don't want
// to bombard stdout with warnings more than once.
// For example, there are X pages that use the CSS sidebar macro `CSSRef` and it
// might contain something like `smartLink(URL + 'oops', ...)` which leads to a
// broken link. But that problem lies with the `CSSRef.ejs` macro, which we
// don't entirely want to swallow and forget. But we don't want to point this
// out on every single page that *uses* that `CSSRef` macro.
function warnBrokenFlawByMacro(macro: string, href: string, notes: string) {
if (!_warned.has(macro)) {
_warned.set(macro, new Set());
}
if (!_warned.get(macro).has(href)) {
_warned.get(macro).add(href);
console.warn(`In ${macro} the smartLink to ${href} is broken! (${notes})`);
}
}
const web = {
// Insert a hyperlink.
link(uri, text, title, target) {
const out = [`", util.htmlEscape(text || uri), "");
return out.join("");
},
// Creates a hyperlink for given document location(href).
// e.g /en-US/docs/Web/HTML/Attributes
//
// For translated content, if the document doesn't exist
// then hyperlink to corresponding en-US document is returned.
smartLink(
this: KumaThis,
href: string,
title: string | null,
content: string | null = null,
subpath: string | null = null,
basepath: string | null = null,
ignoreFlawMacro: string | null = null
) {
let flaw;
let flawAttribute = "";
const page = this.info.getPageByURL(href);
// Get the pathname only (no hash) of the incoming "href" URI.
const hrefpath = this.info.getPathname(href);
// Save the hash portion, if any, for appending to the "href" attribute later.
const hrefhash = new URL(href, DUMMY_BASE_URL).hash;
if (page.url) {
if (hrefpath.toLowerCase() !== page.url.toLowerCase()) {
if (page.url.startsWith(basepath)) {
let suggested = page.url.replace(basepath, "");
if (
/\//.test(suggested) &&
!title &&
basepath.endsWith("/Web/API/")
) {
// This is the exception! When `smartLink` is used from the DOMxRef.ejs
// macro, the xref macro notation should use a `.` instead of a `/`.
// E.g. `{{domxref("GlobalEventHandlers.onload")}}
// because, when displayed we want the HTML to become:
//
//
// GlobalEventHandlers.onload
//
//
// Note the `GlobalEventHandlers.onload` label.
// However, some uses of DOMxRef uses a custom title. E.g.
// {{domxref("WindowOrWorkerGlobalScope/fetch","fetch()")}}
// which needs to become:
//
//
// fetch()
//
//
// So those with titles we ignore.
suggested = suggested.replace(/\//g, ".");
}
if (ignoreFlawMacro) {
warnBrokenFlawByMacro(
ignoreFlawMacro,
href,
`redirects to ${page.url}`
);
} else {
flaw = this.env.recordNonFatalError(
"redirected-link",
`${hrefpath} redirects to ${page.url}`,
{
current: subpath,
suggested,
}
);
flawAttribute = ` data-flaw-src="${util.htmlEscape(
flaw.macroSource
)}"`;
}
} else {
flaw = this.env.recordNonFatalError(
"wrong-xref-macro",
"Wrong xref macro used (consider changing which macro you use). " +
`Error processing path ${href}`,
{
current: subpath,
}
);
flawAttribute = ` data-flaw-src="${util.htmlEscape(
flaw.macroSource
)}"`;
}
}
const titleAttribute = title ? ` title="${title}"` : "";
content ??= page.short_title ?? page.title;
return `${content}`;
}
if (!href.toLowerCase().startsWith("/en-us/")) {
// Before flagging this as a broken-link flaw, see if it's possible to
// change it to the en-US URL instead.
const hrefSplit = href.split("/");
hrefSplit[1] = "en-US";
const enUSPage = this.info.getPageByURL(hrefSplit.join("/"));
if (enUSPage.url) {
// But it's still a flaw. Record it so that translators can write a
// translated document to "fill the hole".
if (!ignoreFlawMacro) {
flaw = this.env.recordNonFatalError(
"broken-link",
`${hrefpath} does not exist but fell back to ${enUSPage.url}`
);
flawAttribute = ` data-flaw-src="${util.htmlEscape(
flaw.macroSource
)}"`;
}
content ??= enUSPage.short_title ?? enUSPage.title;
const title = ONLY_AVAILABLE_IN_ENGLISH(this.env.locale);
return (
'${content}`
);
}
}
if (ignoreFlawMacro) {
warnBrokenFlawByMacro(ignoreFlawMacro, href, "does not exist");
} else {
flaw = this.env.recordNonFatalError(
"broken-link",
`${hrefpath} does not exist`
);
flawAttribute = ` data-flaw-src="${util.htmlEscape(flaw.macroSource)}"`;
}
// Let's get a potentially localized title for when the document is missing.
const titleWhenMissing = (this.mdn as any).getLocalString(
this.web.getJSONData("L10n-Common"),
"summary"
);
content ??= href;
return `${content}`;
},
// Try calling "decodeURIComponent", but if there's an error, just
// return the text unmodified.
safeDecodeURIComponent: util.safeDecodeURIComponent,
// Given a URL, convert all spaces to underscores. This lets us fix a
// bunch of places where templates assume this is done automatically
// by the API, like MindTouch did.
spacesToUnderscores: util.spacesToUnderscores,
// Turn the text content of a header into a slug for use in an ID.
// Remove unsafe characters, and collapse whitespace gaps into
// underscores.
slugify: util.slugify,
// Return specific .json files from the content root
getJSONData(name) {
const filePath = path.join(CONTENT_ROOT, "jsondata", `${name}.json`);
try {
return readJSONDataFile(filePath);
} catch (error) {
console.error(`Tried to read JSON from ${filePath}`, error);
throw error;
}
},
};
const _readJSONDataCache = new Map();
function readJSONDataFile(filePath) {
if (_readJSONDataCache.has(filePath)) {
return _readJSONDataCache.get(filePath);
}
const payload = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (process.env.NODE_ENV === "production") {
_readJSONDataCache.set(filePath, payload);
}
return payload;
}
export default web;