You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/08/31 09:23:21 UTC

(opendal) 01/01: docs: integrate specifications into main navigation

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/unify-specifications-sidebar
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit f479084176f8ae4df5e876b532facc9801d309dc
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Aug 31 17:23:00 2026 +0800

    docs: integrate specifications into main navigation
---
 website/docs/04-specifications/01-overview.mdx     |   9 ++
 .../02-conditional-operations.mdx                  |   8 +
 website/docs/04-specifications/03-metadata.mdx     |   8 +
 website/docs/sidebars.js                           |   9 +-
 website/docusaurus.config.js                       |  20 +--
 website/package.json                               |   5 +-
 website/plugins/specifications-docs-plugin.js      | 162 +++++++++++++++++++++
 website/pnpm-lock.yaml                             |  83 +++--------
 website/specifications.sidebars.js                 |  38 -----
 9 files changed, 225 insertions(+), 117 deletions(-)

diff --git a/website/docs/04-specifications/01-overview.mdx b/website/docs/04-specifications/01-overview.mdx
new file mode 100644
index 000000000..7c86590c4
--- /dev/null
+++ b/website/docs/04-specifications/01-overview.mdx
@@ -0,0 +1,9 @@
+---
+title: OpenDAL Specifications
+description: Portable behavior contracts that OpenDAL promises across supporting services.
+slug: /specifications
+specification_source: README.md
+custom_edit_url: https://github.com/apache/opendal/edit/main/core/core/src/docs/specs/README.md
+---
+
+<Specification />
diff --git a/website/docs/04-specifications/02-conditional-operations.mdx b/website/docs/04-specifications/02-conditional-operations.mdx
new file mode 100644
index 000000000..d5e5d0c0f
--- /dev/null
+++ b/website/docs/04-specifications/02-conditional-operations.mdx
@@ -0,0 +1,8 @@
+---
+title: Conditional operations
+description: Portable predicate, error, and atomicity semantics for conditional operations.
+specification_source: conditional-operations.md
+custom_edit_url: https://github.com/apache/opendal/edit/main/core/core/src/docs/specs/conditional-operations.md
+---
+
+<Specification />
diff --git a/website/docs/04-specifications/03-metadata.mdx b/website/docs/04-specifications/03-metadata.mdx
new file mode 100644
index 000000000..3028cf8bf
--- /dev/null
+++ b/website/docs/04-specifications/03-metadata.mdx
@@ -0,0 +1,8 @@
+---
+title: Metadata
+description: Portable ownership, construction, content length, and operation-boundary semantics for metadata.
+specification_source: metadata.md
+custom_edit_url: https://github.com/apache/opendal/edit/main/core/core/src/docs/specs/metadata.md
+---
+
+<Specification />
diff --git a/website/docs/sidebars.js b/website/docs/sidebars.js
index 8e1cecc1e..c5ca83ece 100644
--- a/website/docs/sidebars.js
+++ b/website/docs/sidebars.js
@@ -46,9 +46,14 @@ const sidebars = {
     'vision',
     'concepts',
     {
-      type: 'link',
+      type: 'category',
       label: 'Specifications',
-      href: '/docs/specifications/',
+      link: { type: 'doc', id: 'specifications/overview' },
+      collapsed: true,
+      items: [
+        'specifications/conditional-operations',
+        'specifications/metadata',
+      ],
     },
     // Divider between the concept docs and the per-language guides.
     {
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index cede27cb4..9169751bc 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -28,6 +28,10 @@ const {
   getWebsiteSettings,
   orderByPathDepth,
 } = require("./config/site-helpers");
+const {
+  parseSpecificationFrontMatter,
+  remarkIncludeSpecification,
+} = require("./plugins/specifications-docs-plugin");
 
 const repoAddress = "https://github.com/apache/opendal";
 
@@ -55,6 +59,7 @@ const config = {
   onBrokenLinks: "throw",
   markdown: {
     format: "detect",
+    parseFrontMatter: parseSpecificationFrontMatter,
     hooks: {
       onBrokenMarkdownLinks: "throw",
     }
@@ -84,6 +89,7 @@ const config = {
           editUrl: "https://github.com/apache/opendal/tree/main/website/",
           showLastUpdateAuthor: true,
           showLastUpdateTime: true,
+          beforeDefaultRemarkPlugins: [remarkIncludeSpecification],
           remarkPlugins: [require("./plugins/remark-include-code")],
         },
         blog: {
@@ -112,19 +118,7 @@ const config = {
   ],
 
   plugins: [
-    [
-      "@docusaurus/plugin-content-docs",
-      {
-        id: "specifications",
-        path: "../core/core/src/docs/specs",
-        routeBasePath: "docs/specifications",
-        sidebarPath: require.resolve("./specifications.sidebars.js"),
-        editUrl: ({ docPath }) =>
-          `${repoAddress}/edit/main/core/core/src/docs/specs/${docPath}`,
-        showLastUpdateAuthor: true,
-        showLastUpdateTime: true,
-      },
-    ],
+    require.resolve("./plugins/specifications-docs-plugin"),
     [
       "@docusaurus/plugin-content-docs",
       {
diff --git a/website/package.json b/website/package.json
index a5064ac37..0a5e49279 100644
--- a/website/package.json
+++ b/website/package.json
@@ -27,7 +27,10 @@
     "prism-react-renderer": "^2.4.1",
     "react": "^19.2.7",
     "react-dom": "^19.2.7",
-    "semver": "^7.8.4"
+    "remark-gfm": "^4.0.1",
+    "remark-parse": "^11.0.0",
+    "semver": "^7.8.4",
+    "unified": "^11.0.4"
   },
   "devDependencies": {
     "@docusaurus/module-type-aliases": "^3.10.1",
diff --git a/website/plugins/specifications-docs-plugin.js b/website/plugins/specifications-docs-plugin.js
new file mode 100644
index 000000000..a9daa7d2d
--- /dev/null
+++ b/website/plugins/specifications-docs-plugin.js
@@ -0,0 +1,162 @@
+/*
+ * 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;
diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml
index b4d0f4a6c..ce9456f03 100644
--- a/website/pnpm-lock.yaml
+++ b/website/pnpm-lock.yaml
@@ -44,9 +44,18 @@ importers:
       react-dom:
         specifier: ^19.2.7
         version: 19.2.7(react@19.2.7)
+      remark-gfm:
+        specifier: ^4.0.1
+        version: 4.0.1
+      remark-parse:
+        specifier: ^11.0.0
+        version: 11.0.0
       semver:
         specifier: ^7.8.4
         version: 7.8.4
+      unified:
+        specifier: ^11.0.4
+        version: 11.0.4
     devDependencies:
       '@docusaurus/module-type-aliases':
         specifier: ^3.10.1
@@ -1274,10 +1283,6 @@ packages:
   '@jridgewell/gen-mapping@0.3.12':
     resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
 
-  '@jridgewell/gen-mapping@0.3.5':
-    resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
-    engines: {node: '>=6.0.0'}
-
   '@jridgewell/remapping@2.3.5':
     resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
 
@@ -1285,19 +1290,9 @@ packages:
     resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
     engines: {node: '>=6.0.0'}
 
-  '@jridgewell/set-array@1.2.1':
-    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
-    engines: {node: '>=6.0.0'}
-
   '@jridgewell/source-map@0.3.11':
     resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
 
-  '@jridgewell/sourcemap-codec@1.4.10':
-    resolution: {integrity: sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==}
-
-  '@jridgewell/sourcemap-codec@1.4.14':
-    resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
-
   '@jridgewell/sourcemap-codec@1.5.0':
     resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
 
@@ -5652,12 +5647,6 @@ packages:
     resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
     engines: {node: '>=4'}
 
-  unified@11.0.0:
-    resolution: {integrity: sha512-65zgPyv0vyXnJpw4fbGmoXjP0/6cBmnnesl4lSPGU2tYuzLWtuicRBFcaV6kgzitGrBHj6pgXYomMw1VQe/cQg==}
-
-  unified@11.0.3:
-    resolution: {integrity: sha512-jlCV402P+YDcFcB2VcN/n8JasOddqIiaxv118wNBoZXEhOn+lYG7BR4Bfg2BwxvlK58dwbuH2w7GX2esAjL6Mg==}
-
   unified@11.0.4:
     resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==}
 
@@ -7420,7 +7409,7 @@ snapshots:
       remark-gfm: 4.0.1
       stringify-object: 3.3.0
       tslib: 2.8.1
-      unified: 11.0.3
+      unified: 11.0.4
       unist-util-visit: 5.1.0
       url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.2(@swc/core@1.15.41)(postcss@8.5.15)))(webpack@5.107.2(@swc/core@1.15.41)(postcss@8.5.15))
       vfile: 6.0.3
@@ -8316,36 +8305,24 @@ snapshots:
       '@jridgewell/sourcemap-codec': 1.5.0
       '@jridgewell/trace-mapping': 0.3.31
 
-  '@jridgewell/gen-mapping@0.3.5':
-    dependencies:
-      '@jridgewell/set-array': 1.2.1
-      '@jridgewell/sourcemap-codec': 1.4.10
-      '@jridgewell/trace-mapping': 0.3.31
-
   '@jridgewell/remapping@2.3.5':
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/gen-mapping': 0.3.12
       '@jridgewell/trace-mapping': 0.3.31
 
   '@jridgewell/resolve-uri@3.1.2': {}
 
-  '@jridgewell/set-array@1.2.1': {}
-
   '@jridgewell/source-map@0.3.11':
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/gen-mapping': 0.3.12
       '@jridgewell/trace-mapping': 0.3.31
 
-  '@jridgewell/sourcemap-codec@1.4.10': {}
-
-  '@jridgewell/sourcemap-codec@1.4.14': {}
-
   '@jridgewell/sourcemap-codec@1.5.0': {}
 
   '@jridgewell/trace-mapping@0.3.31':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
-      '@jridgewell/sourcemap-codec': 1.4.14
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)':
     dependencies:
@@ -8498,7 +8475,7 @@ snapshots:
       remark-parse: 11.0.0
       remark-rehype: 11.1.2
       source-map: 0.7.6
-      unified: 11.0.3
+      unified: 11.0.4
       unist-util-position-from-estree: 2.0.0
       unist-util-stringify-position: 4.0.0
       unist-util-visit: 5.1.0
@@ -12662,7 +12639,7 @@ snapshots:
       '@types/mdast': 4.0.4
       mdast-util-directive: 3.1.0
       micromark-extension-directive: 3.0.2
-      unified: 11.0.3
+      unified: 11.0.4
     transitivePeerDependencies:
       - supports-color
 
@@ -12679,7 +12656,7 @@ snapshots:
       '@types/mdast': 4.0.4
       mdast-util-frontmatter: 2.0.1
       micromark-extension-frontmatter: 2.0.0
-      unified: 11.0.3
+      unified: 11.0.4
     transitivePeerDependencies:
       - supports-color
 
@@ -12690,7 +12667,7 @@ snapshots:
       micromark-extension-gfm: 3.0.0
       remark-parse: 11.0.0
       remark-stringify: 11.0.0
-      unified: 11.0.3
+      unified: 11.0.4
     transitivePeerDependencies:
       - supports-color
 
@@ -12706,7 +12683,7 @@ snapshots:
       '@types/mdast': 4.0.4
       mdast-util-from-markdown: 2.0.3
       micromark-util-types: 2.0.0
-      unified: 11.0.0
+      unified: 11.0.4
     transitivePeerDependencies:
       - supports-color
 
@@ -12715,14 +12692,14 @@ snapshots:
       '@types/hast': 3.0.4
       '@types/mdast': 4.0.4
       mdast-util-to-hast: 13.2.1
-      unified: 11.0.0
+      unified: 11.0.4
       vfile: 6.0.3
 
   remark-stringify@11.0.0:
     dependencies:
       '@types/mdast': 4.0.4
       mdast-util-to-markdown: 2.0.0
-      unified: 11.0.0
+      unified: 11.0.4
 
   renderkid@3.0.0:
     dependencies:
@@ -13262,26 +13239,6 @@ snapshots:
 
   unicode-property-aliases-ecmascript@2.2.0: {}
 
-  unified@11.0.0:
-    dependencies:
-      '@types/unist': 3.0.3
-      '@ungap/structured-clone': 1.3.1
-      bail: 2.0.2
-      devlop: 1.1.0
-      is-plain-obj: 4.1.0
-      trough: 2.2.0
-      vfile: 6.0.3
-
-  unified@11.0.3:
-    dependencies:
-      '@types/unist': 3.0.3
-      bail: 2.0.2
-      devlop: 1.1.0
-      extend: 3.0.2
-      is-plain-obj: 4.1.0
-      trough: 2.2.0
-      vfile: 6.0.3
-
   unified@11.0.4:
     dependencies:
       '@types/unist': 3.0.3
diff --git a/website/specifications.sidebars.js b/website/specifications.sidebars.js
deleted file mode 100644
index fbc85f1b4..000000000
--- a/website/specifications.sidebars.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.
- */
-
-// @ts-check
-
-/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
-const sidebars = {
-  specs: [
-    {
-      type: "doc",
-      id: "README",
-      label: "Overview",
-    },
-    {
-      type: "doc",
-      id: "conditional-operations",
-      label: "Conditional operations",
-    },
-  ],
-};
-
-module.exports = sidebars;