mirror of
https://github.com/KytyPS5/kytyps5.github.io.git
synced 2026-10-09 08:49:37 -07:00
117 lines
4.3 KiB
JavaScript
117 lines
4.3 KiB
JavaScript
/**
|
|
* Parsing helpers shared by the issue intake pipeline:
|
|
* - scripts/issue-to-compat.mjs (issue body → report)
|
|
* - scripts/sync-status-issues.mjs (issue body → mirror issue title)
|
|
*
|
|
* The Game Emulation Status Report template (KytyPS5 repo) renders every
|
|
* question under a `### <label>` heading with the answer below it; `## <group>`
|
|
* headings separate template sections. parseIssueBody() turns that raw body
|
|
* into `label → text` pairs, and cleanField()/normalizeOs() fold the
|
|
* free-text answers onto the site's canonical values. Kept dependency-free
|
|
* (node builtins only) so the workflows can import it anywhere.
|
|
*/
|
|
|
|
const GROUP_HEADINGS = new Set([
|
|
"game and build",
|
|
"test result",
|
|
"hardware",
|
|
"evidence",
|
|
"system information",
|
|
"environment",
|
|
"source",
|
|
"overrides",
|
|
]);
|
|
|
|
function isGroupHeading(line) {
|
|
const m = line.match(/^##\s+(.+?)\s*$/);
|
|
if (!m) return false;
|
|
return GROUP_HEADINGS.has(m[1].trim().toLowerCase());
|
|
}
|
|
|
|
/**
|
|
* Parse a GitHub-issue-form body into its `### Section label` → value pairs.
|
|
* The form renders each answer under its heading (with a blank line), so
|
|
* multi-line textarea answers are captured whole. Known `## ` group headings
|
|
* ("## Game and build", "## Test result", mirror "## Source" footer, …) close
|
|
* the current field without opening a new one. Arbitrary user headings inside
|
|
* textareas (e.g. `## Logs`) are preserved in the field value.
|
|
*/
|
|
export function parseIssueBody(body) {
|
|
const sections = {};
|
|
const lines = String(body ?? "").split(/\r?\n/);
|
|
let current = null;
|
|
for (const line of lines) {
|
|
const field = line.match(/^###\s+(.+?)\s*$/);
|
|
if (field) {
|
|
current = field[1].trim();
|
|
sections[current] = "";
|
|
continue;
|
|
}
|
|
if (isGroupHeading(line)) {
|
|
current = null; // group separator — end the current field
|
|
continue;
|
|
}
|
|
if (current !== null) sections[current] += line + "\n";
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
/** Unanswered optional form fields render as "_No response_" — treat as absent. */
|
|
export function cleanField(sections, label) {
|
|
const v = sections[label] ?? "";
|
|
return v.replace(/_No response_/g, "").replace(/_Unknown_/g, "").trim();
|
|
}
|
|
|
|
/** The new template's OS field is free text — fold it into windows/linux/macos. */
|
|
export const OS_ALIASES = [
|
|
[/win(?:dows)?(?: ?10| ?11| server)?|microsoft|^ms/i, "windows"],
|
|
[/\b(?:mac(?:os|book)?|os\s*x|osx|darwin|apple)\b/i, "macos"],
|
|
[/linux|ubuntu|debian|arch|fedora|linux mint|mint|pop.?os|manjaro|opensuse|steamos|steam ?deck|cachy(?:os)?|bazzite|nobara|gentoo|void|nixos|endeavour(?:os)?/i, "linux"],
|
|
];
|
|
|
|
export function normalizeOs(os) {
|
|
const v = String(os ?? "").trim().toLowerCase();
|
|
for (const [re, canonical] of OS_ALIASES) if (re.test(v)) return canonical;
|
|
return undefined;
|
|
}
|
|
|
|
export const STATUSES = ["doesnt-boot", "logo", "main-menu", "in-game"];
|
|
|
|
const TEMPLATE_STATUS = {
|
|
"doesn't boot": "doesnt-boot",
|
|
"doesnt boot": "doesnt-boot",
|
|
"does not boot": "doesnt-boot",
|
|
logo: "logo",
|
|
intro: "logo",
|
|
"main menu": "main-menu",
|
|
"in game": "in-game",
|
|
playable: "in-game",
|
|
perfect: "in-game",
|
|
nothing: "doesnt-boot",
|
|
boots: "logo",
|
|
menus: "main-menu",
|
|
ingame: "in-game",
|
|
"playable-low-fps": "in-game",
|
|
};
|
|
|
|
/**
|
|
* Fold free-text or template status options onto the canonical status ladder:
|
|
* doesnt-boot → logo → main-menu → in-game
|
|
*/
|
|
export function normalizeStatus(status) {
|
|
const v = String(status ?? "").trim().toLowerCase();
|
|
if (!v) return undefined;
|
|
if (STATUSES.includes(v)) return v;
|
|
if (TEMPLATE_STATUS[v]) return TEMPLATE_STATUS[v];
|
|
|
|
// Negative boot phrases (crashed on boot, failed on boot, freezes on boot, etc.)
|
|
// Must run BEFORE the logo regex so "crashed on boot" does not match "boots?" as logo!
|
|
if (/\b(?:doesn'?t\s*boot|does\s*not\s*boot|not\s*boot(?:ing)?|nothing|no\s*boot|can(?:not|'?t|\s+not)\s*boot|could(?:not|'?t|\s+not)\s*boot|unable\s+to\s+boot|(?:crash(?:es|ed|ing)?|fail(?:s|ed|ing)?|freeze[sd]?|freezing|froze|hang(?:s|ed|ing)?|stuck|black\s*screen)(?:\s+\w+){0,2}\s+(?:on|to|at|during)\s+boot)\b/i.test(v)) return "doesnt-boot";
|
|
if (/\b(?:in[- ]?game|playable|perfect)\b/i.test(v)) return "in-game";
|
|
if (/\b(?:main[- ]?menu|menus?)\b/i.test(v)) return "main-menu";
|
|
if (/\b(?:logo|splash|intro|boots?)\b/i.test(v)) return "logo";
|
|
|
|
return undefined;
|
|
}
|
|
|