| 171 | } |
| 172 | |
| 173 | function parseReadme(file) { |
| 174 | const lines = fs.readFileSync(file, "utf8").split(/\r?\n/); |
| 175 | |
| 176 | let name = null; |
| 177 | for (const l of lines) { |
| 178 | const m = l.match(/^#\s+(.+)/); |
| 179 | if (m) { |
| 180 | name = m[1].trim(); |
| 181 | break; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | // blurb: first plain paragraph (skip headings, images, quotes, code fences), |
| 186 | // joining the wrapped lines of that paragraph back into one sentence. |
| 187 | let blurb = ""; |
| 188 | let inCode = false; |
| 189 | const para = []; |
| 190 | for (const raw of lines) { |
| 191 | const l = raw.trim(); |
| 192 | if (l.startsWith("```")) { |
| 193 | inCode = !inCode; |
| 194 | continue; |
| 195 | } |
| 196 | if (inCode) continue; |
| 197 | if (!l) { |
| 198 | if (para.length) break; |
| 199 | continue; |
| 200 | } |
| 201 | if (l.startsWith("#") || l.startsWith("![") || l.startsWith(">")) { |
| 202 | if (para.length) break; |
| 203 | continue; |
| 204 | } |
| 205 | if (l.startsWith("- ") || l.startsWith("* ") || /^\d+\.\s/.test(l)) { |
| 206 | if (para.length) break; |
| 207 | continue; |
| 208 | } |
| 209 | para.push(l); |
| 210 | } |
| 211 | blurb = para.join(" "); |
| 212 | blurb = blurb |
| 213 | .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") |
| 214 | .replace(/[*`_]/g, "") |
| 215 | .trim(); |
| 216 | if (blurb.length > 150) blurb = blurb.slice(0, 147).trimEnd() + "…"; |
| 217 | |
| 218 | return { name, blurb }; |
| 219 | } |
| 220 | |
| 221 | const folders = fs |
| 222 | .readdirSync(PROJECTS_DIR, { withFileTypes: true }) |