blob: a9daa7d2da1a3a905c4005d631df97cd94896e07 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const REPOSITORY_DIR = path.resolve(__dirname, "../..");
const SPECIFICATIONS_DIR = path.resolve(
REPOSITORY_DIR,
"core/core/src/docs/specs"
);
let markdownParser;
const lastUpdates = new Map();
async function getMarkdownParser() {
if (!markdownParser) {
markdownParser = Promise.all([
import("remark-gfm"),
import("remark-parse"),
import("unified"),
]).then(([{ default: remarkGfm }, { default: remarkParse }, { unified }]) =>
unified().use(remarkParse).use(remarkGfm)
);
}
return markdownParser;
}
function resolveSpecification(relativePath) {
const resolved = path.resolve(SPECIFICATIONS_DIR, relativePath);
if (
path.extname(resolved) !== ".md" ||
!resolved.startsWith(`${SPECIFICATIONS_DIR}${path.sep}`)
) {
throw new Error(
`Specification include must reference a Markdown file under ${SPECIFICATIONS_DIR}: ${relativePath}`
);
}
return resolved;
}
function isSpecificationInclude(node) {
if (node.type !== "mdxJsxFlowElement" || node.name !== "Specification") {
return false;
}
if (node.attributes.length !== 0) {
throw new Error("Specification include does not accept attributes");
}
return true;
}
async function includeSpecifications(node, source) {
if (!Array.isArray(node.children)) {
return 0;
}
const parser = await getMarkdownParser();
let includeCount = 0;
for (let index = 0; index < node.children.length; index += 1) {
const child = node.children[index];
if (isSpecificationInclude(child)) {
const included = parser.parse(fs.readFileSync(source, "utf8"));
node.children.splice(index, 1, ...included.children);
index += included.children.length - 1;
includeCount += 1;
} else {
includeCount += await includeSpecifications(child, source);
}
}
return includeCount;
}
function remarkIncludeSpecification() {
return async (tree, file) => {
const relativePath = file.data.frontMatter.specification_source;
if (relativePath === undefined) {
return;
}
if (typeof relativePath !== "string") {
throw new Error(`Specification source must be a string: ${file.path}`);
}
const source = resolveSpecification(relativePath);
const includeCount = await includeSpecifications(tree, source);
if (includeCount !== 1) {
throw new Error(
`Specification page must contain exactly one include: ${file.path}`
);
}
};
}
function readLastUpdate(source) {
if (!lastUpdates.has(source)) {
let lastUpdate;
try {
const relativePath = path.relative(REPOSITORY_DIR, source);
const output = execFileSync(
"git",
["log", "-1", "--format=%aN%x00%aI", "--", relativePath],
{ cwd: REPOSITORY_DIR, encoding: "utf8" }
).trimEnd();
if (output) {
const [author, date] = output.split("\0");
lastUpdate = { author, date };
}
} catch {
lastUpdate = undefined;
}
lastUpdates.set(source, lastUpdate);
}
return lastUpdates.get(source);
}
async function parseSpecificationFrontMatter({
filePath,
fileContent,
defaultParseFrontMatter,
}) {
const parsed = await defaultParseFrontMatter({ filePath, fileContent });
const relativePath = parsed.frontMatter.specification_source;
if (relativePath === undefined) {
return parsed;
}
if (typeof relativePath !== "string") {
throw new Error(`Specification source must be a string: ${filePath}`);
}
// The wrapper owns website metadata, but the canonical file owns its history.
const lastUpdate = readLastUpdate(resolveSpecification(relativePath));
if (lastUpdate) {
parsed.frontMatter.last_update = lastUpdate;
}
return parsed;
}
module.exports = function specificationsDocsPlugin() {
return {
name: "opendal-specifications-docs",
getPathsToWatch() {
return [path.join(SPECIFICATIONS_DIR, "**/*.md")];
},
};
};
module.exports.remarkIncludeSpecification = remarkIncludeSpecification;
module.exports.parseSpecificationFrontMatter = parseSpecificationFrontMatter;