mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-12 21:30:02 -04:00
120 lines
3.7 KiB
JavaScript
120 lines
3.7 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const FM_RE = /^---\n([\s\S]*?)\n---\n/;
|
|
|
|
/**
|
|
* Parse YAML-ish frontmatter from a markdown string.
|
|
* Returns { fields: { key: string }, body: string, raw: string }.
|
|
* `fields` maps lowercase keys to their raw string values (no YAML arrays).
|
|
*/
|
|
function parseFrontmatter(content) {
|
|
const match = content.match(FM_RE);
|
|
if (!match) return { fields: {}, body: content, raw: "" };
|
|
|
|
const raw = match[1];
|
|
const body = content.slice(match[0].length);
|
|
const fields = {};
|
|
|
|
for (const line of raw.split("\n")) {
|
|
const kv = line.match(/^(\w[\w_-]*):\s*(.*)/);
|
|
if (kv) fields[kv[1]] = kv[2].trim();
|
|
}
|
|
|
|
return { fields, body, raw };
|
|
}
|
|
|
|
/**
|
|
* Read a YAML list field out of a raw frontmatter block (the `raw` from parseFrontmatter).
|
|
* `fields` cannot carry these: a block list leaves the key's own line empty.
|
|
* Handles both the block form used by every page —
|
|
* aliases:
|
|
* - bluetooth
|
|
* - usb
|
|
* — and the inline form `aliases: [bluetooth, usb]`. Returns [] when the key is absent.
|
|
*/
|
|
function parseListField(rawFrontmatter, key) {
|
|
const lines = rawFrontmatter.split("\n");
|
|
const values = [];
|
|
let inList = false;
|
|
|
|
for (const line of lines) {
|
|
if (!inList) {
|
|
const head = line.match(new RegExp(`^${key}:\\s*(.*)$`));
|
|
if (!head) continue;
|
|
|
|
const inline = head[1].trim();
|
|
if (inline.startsWith("[")) {
|
|
// Split on commas that sit outside quotes: a quoted scalar may legally contain one
|
|
// (aliases: ["serial, USB"]), and a naive split would report it as two aliases and
|
|
// then flag phantom drift against DocBundleLoader.
|
|
return splitInlineList(inline.replace(/^\[|\]$/g, ""));
|
|
}
|
|
if (inline.length > 0) return [inline.replace(/^["']|["']$/g, "")];
|
|
inList = true;
|
|
continue;
|
|
}
|
|
|
|
const item = line.match(/^\s+-\s+(.*)$/);
|
|
if (item) {
|
|
values.push(item[1].trim().replace(/^["']|["']$/g, ""));
|
|
continue;
|
|
}
|
|
// Any non-item line at this point ends the list.
|
|
if (line.trim().length > 0) break;
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
/** Discover all .md page slugs under docs/{section}/ */
|
|
function discoverSlugs(docsDir, section) {
|
|
const dir = path.join(docsDir, section);
|
|
if (!fs.existsSync(dir)) return new Set();
|
|
return new Set(
|
|
fs.readdirSync(dir)
|
|
.filter(f => f.endsWith(".md"))
|
|
.map(f => f.replace(/\.md$/, "")),
|
|
);
|
|
}
|
|
|
|
/** Iterate all doc pages, calling fn(filePath, slug, section) */
|
|
function forEachDocPage(docsDir, fn) {
|
|
for (const section of ["user", "developer"]) {
|
|
const dir = path.join(docsDir, section);
|
|
if (!fs.existsSync(dir)) continue;
|
|
for (const file of fs.readdirSync(dir).filter(f => f.endsWith(".md")).sort()) {
|
|
fn(path.join(dir, file), file.replace(/\.md$/, ""), section);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { parseFrontmatter, parseListField, discoverSlugs, forEachDocPage };
|
|
|
|
/**
|
|
* Split the body of an inline YAML flow sequence on top-level commas only, honouring single and
|
|
* double quotes. Returns trimmed, unquoted, non-empty values.
|
|
*/
|
|
function splitInlineList(body) {
|
|
const out = [];
|
|
let cur = "";
|
|
let quote = null;
|
|
for (const ch of body) {
|
|
if (quote) {
|
|
if (ch === quote) quote = null;
|
|
else cur += ch;
|
|
} else if (ch === '"' || ch === "'") {
|
|
quote = ch;
|
|
} else if (ch === ",") {
|
|
out.push(cur);
|
|
cur = "";
|
|
} else {
|
|
cur += ch;
|
|
}
|
|
}
|
|
out.push(cur);
|
|
return out.map(v => v.trim()).filter(v => v.length > 0);
|
|
}
|