Tools for the Atmosphere tools.slices.network
quickslice atproto html

feat(bugs): add markdown richtext facets for bug descriptions

Add support for markdown formatting in bug descriptions and steps to reproduce:

- **bold** with **text**
- *italic* with *text* or _text_
- `inline code` with backticks
- ```fenced code blocks``` with optional language

Implementation:
- New lexicon: network.slices.tools.richtext.facet with bold/italic/code/codeBlock/link types
- Parser detects markdown syntax and creates facets (delimiters preserved for editing)
- Renderer strips delimiters and outputs formatted HTML
- GraphQL queries updated to fetch __typename for facet type detection
- CSS styles for all formatting types
- Form hints show supported syntax

+1031 -11
+157 -9
bugs.html
··· 289 289 color: var(--accent-hover); 290 290 } 291 291 292 + .facet-bold { 293 + font-weight: 600; 294 + } 295 + 296 + .facet-italic { 297 + font-style: italic; 298 + } 299 + 300 + .facet-code { 301 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 302 + background: var(--bg-hover); 303 + padding: 0.125rem 0.25rem; 304 + border-radius: 0.25rem; 305 + font-size: 0.875em; 306 + } 307 + 308 + .facet-codeblock { 309 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 310 + background: var(--bg-hover); 311 + padding: 1rem; 312 + border-radius: 0.5rem; 313 + font-size: 0.875em; 314 + overflow-x: auto; 315 + margin: 0.5rem 0; 316 + white-space: pre; 317 + } 318 + 319 + .facet-codeblock code { 320 + background: none; 321 + padding: 0; 322 + } 323 + 324 + .form-hint { 325 + font-size: 0.75rem; 326 + color: var(--text-secondary); 327 + margin-top: 0.25rem; 328 + } 329 + 292 330 /* Loading spinner */ 293 331 .spinner { 294 332 width: 24px; ··· 1337 1375 function parseFacets(text) { 1338 1376 if (!text) return { text, facets: null }; 1339 1377 1340 - const urlRegex = /https?:\/\/[^\s<>"\]\)]+/gi; 1341 1378 const facets = []; 1379 + const encoder = new TextEncoder(); 1342 1380 1381 + // Track which character positions are already claimed to avoid overlaps 1382 + const claimed = new Set(); 1383 + 1384 + // Process fenced code blocks FIRST (before inline patterns) 1385 + const codeBlockRegex = /```(\w*)\n?([\s\S]*?)```/g; 1386 + let codeBlockMatch; 1387 + while ((codeBlockMatch = codeBlockRegex.exec(text)) !== null) { 1388 + const start = codeBlockMatch.index; 1389 + const end = start + codeBlockMatch[0].length; 1390 + const lang = codeBlockMatch[1] || null; 1391 + 1392 + // Claim these positions 1393 + for (let i = start; i < end; i++) { 1394 + claimed.add(i); 1395 + } 1396 + 1397 + const byteStart = encoder.encode(text.slice(0, start)).length; 1398 + const byteEnd = byteStart + encoder.encode(codeBlockMatch[0]).length; 1399 + 1400 + const feature = { $type: 'network.slices.tools.richtext.facet#codeBlock' }; 1401 + if (lang) feature.lang = lang; 1402 + 1403 + facets.push({ 1404 + index: { byteStart, byteEnd }, 1405 + features: [feature], 1406 + }); 1407 + } 1408 + 1409 + // Process inline markdown patterns (order matters: bold before italic) 1410 + const patterns = [ 1411 + { regex: /\*\*(.+?)\*\*/g, type: 'bold' }, 1412 + { regex: /\*(.+?)\*/g, type: 'italic' }, 1413 + { regex: /_(.+?)_/g, type: 'italic' }, 1414 + { regex: /`([^`]+)`/g, type: 'code' }, // Single backticks, excluding empty 1415 + ]; 1416 + 1417 + for (const { regex, type } of patterns) { 1418 + let match; 1419 + while ((match = regex.exec(text)) !== null) { 1420 + const start = match.index; 1421 + const end = start + match[0].length; 1422 + 1423 + // Skip if any part of this match overlaps with already claimed positions 1424 + let overlaps = false; 1425 + for (let i = start; i < end; i++) { 1426 + if (claimed.has(i)) { 1427 + overlaps = true; 1428 + break; 1429 + } 1430 + } 1431 + if (overlaps) continue; 1432 + 1433 + // Claim these positions 1434 + for (let i = start; i < end; i++) { 1435 + claimed.add(i); 1436 + } 1437 + 1438 + // Calculate byte positions (facet covers full match including delimiters) 1439 + const byteStart = encoder.encode(text.slice(0, start)).length; 1440 + const byteEnd = byteStart + encoder.encode(match[0]).length; 1441 + 1442 + facets.push({ 1443 + index: { byteStart, byteEnd }, 1444 + features: [{ $type: `network.slices.tools.richtext.facet#${type}` }], 1445 + }); 1446 + } 1447 + } 1448 + 1449 + // Process URLs 1450 + const urlRegex = /https?:\/\/[^\s<>"\]\)]+/gi; 1343 1451 let match; 1344 1452 while ((match = urlRegex.exec(text)) !== null) { 1345 - const url = match[0]; 1346 - const charStart = match.index; 1347 - const charEnd = charStart + url.length; 1348 - 1349 - const byteStart = new TextEncoder().encode(text.slice(0, charStart)).length; 1350 - const byteEnd = new TextEncoder().encode(text.slice(0, charEnd)).length; 1453 + const byteStart = encoder.encode(text.slice(0, match.index)).length; 1454 + const byteEnd = byteStart + encoder.encode(match[0]).length; 1351 1455 1352 1456 facets.push({ 1353 1457 index: { byteStart, byteEnd }, 1354 - features: [{ $type: "app.bsky.richtext.facet#link", uri: url }], 1458 + features: [{ $type: "network.slices.tools.richtext.facet#link", uri: match[0] }], 1355 1459 }); 1356 1460 } 1357 1461 1462 + // Sort facets by position 1463 + facets.sort((a, b) => a.index.byteStart - b.index.byteStart); 1464 + 1358 1465 return { 1359 - text, 1466 + text, // Keep original text with delimiters 1360 1467 facets: facets.length > 0 ? facets : null, 1361 1468 }; 1362 1469 } ··· 1389 1496 1390 1497 const facetText = decoder.decode(bytes.slice(facet.index.byteStart, facet.index.byteEnd)); 1391 1498 const link = facet.features.find((f) => f.uri); 1499 + const bold = facet.features.find((f) => f.$type?.endsWith('#bold') || f.__typename === 'NetworkSlicesToolsRichtextFacetBold'); 1500 + const italic = facet.features.find((f) => f.$type?.endsWith('#italic') || f.__typename === 'NetworkSlicesToolsRichtextFacetItalic'); 1501 + const code = facet.features.find((f) => f.$type?.endsWith('#code') || f.__typename === 'NetworkSlicesToolsRichtextFacetCode'); 1502 + const codeBlock = facet.features.find((f) => f.$type?.endsWith('#codeBlock') || f.__typename === 'NetworkSlicesToolsRichtextFacetCodeBlock'); 1503 + 1504 + // Strip markdown delimiters for display 1505 + let displayText = facetText; 1506 + if (codeBlock) { 1507 + // Extract code content from ```lang\ncode``` format 1508 + const match = facetText.match(/^```\w*\n?([\s\S]*?)```$/); 1509 + displayText = match ? match[1] : facetText; 1510 + } else if (bold) { 1511 + displayText = facetText.replace(/^\*\*(.+)\*\*$/, '$1'); 1512 + } else if (italic) { 1513 + displayText = facetText.replace(/^\*(.+)\*$/, '$1').replace(/^_(.+)_$/, '$1'); 1514 + } else if (code) { 1515 + displayText = facetText.replace(/^`(.+)`$/, '$1'); 1516 + } 1392 1517 1393 1518 if (link) { 1394 1519 result += `<a href="${esc(link.uri)}" target="_blank" rel="noopener noreferrer" class="facet-link">${esc(facetText)}</a>`; 1520 + } else if (codeBlock) { 1521 + const lang = codeBlock.lang || ''; 1522 + result += `<pre class="facet-codeblock${lang ? ` language-${esc(lang)}` : ''}"><code>${esc(displayText)}</code></pre>`; 1523 + } else if (code) { 1524 + result += `<code class="facet-code">${esc(displayText)}</code>`; 1525 + } else if (bold && italic) { 1526 + result += `<strong class="facet-bold"><em class="facet-italic">${esc(displayText)}</em></strong>`; 1527 + } else if (bold) { 1528 + result += `<strong class="facet-bold">${esc(displayText)}</strong>`; 1529 + } else if (italic) { 1530 + result += `<em class="facet-italic">${esc(displayText)}</em>`; 1395 1531 } else { 1396 1532 result += esc(facetText); 1397 1533 } ··· 1690 1826 descriptionFacets { 1691 1827 index { byteStart byteEnd } 1692 1828 features { 1829 + __typename 1693 1830 ... on AppBskyRichtextFacetLink { uri } 1831 + ... on NetworkSlicesToolsRichtextFacetLink { uri } 1694 1832 } 1695 1833 } 1696 1834 stepsToReproduce 1697 1835 stepsToReproduceFacets { 1698 1836 index { byteStart byteEnd } 1699 1837 features { 1838 + __typename 1700 1839 ... on AppBskyRichtextFacetLink { uri } 1840 + ... on NetworkSlicesToolsRichtextFacetLink { uri } 1701 1841 } 1702 1842 } 1703 1843 severity ··· 1780 1920 messageFacets { 1781 1921 index { byteStart byteEnd } 1782 1922 features { 1923 + __typename 1783 1924 ... on AppBskyRichtextFacetLink { uri } 1925 + ... on NetworkSlicesToolsRichtextFacetLink { uri } 1784 1926 } 1785 1927 } 1786 1928 actorHandle ··· 1842 1984 bodyFacets { 1843 1985 index { byteStart byteEnd } 1844 1986 features { 1987 + __typename 1845 1988 ... on AppBskyRichtextFacetLink { uri } 1989 + ... on NetworkSlicesToolsRichtextFacetLink { uri } 1846 1990 } 1847 1991 } 1848 1992 parent ··· 3331 3475 <div class="form-group"> 3332 3476 <label for="bug-description">Description *</label> 3333 3477 <textarea id="bug-description" required maxlength="3000" placeholder="What happened? What did you expect?"></textarea> 3478 + <div class="form-hint">Supports **bold**, *italic*, \`code\`, and \`\`\`code blocks\`\`\`</div> 3334 3479 </div> 3335 3480 3336 3481 <div class="form-group"> 3337 3482 <label for="bug-steps">Steps to Reproduce *</label> 3338 3483 <textarea id="bug-steps" required maxlength="1500" placeholder="1. Go to...\n2. Click on...\n3. See error"></textarea> 3484 + <div class="form-hint">Supports **bold**, *italic*, \`code\`, and \`\`\`code blocks\`\`\`</div> 3339 3485 </div> 3340 3486 3341 3487 <div class="form-group"> ··· 3486 3632 <div class="form-group"> 3487 3633 <label for="bug-description">Description *</label> 3488 3634 <textarea id="bug-description" required maxlength="3000">${esc(bug.description)}</textarea> 3635 + <div class="form-hint">Supports **bold**, *italic*, \`code\`, and \`\`\`code blocks\`\`\`</div> 3489 3636 </div> 3490 3637 3491 3638 <div class="form-group"> 3492 3639 <label for="bug-steps">Steps to Reproduce *</label> 3493 3640 <textarea id="bug-steps" required maxlength="1500">${esc(bug.stepsToReproduce)}</textarea> 3641 + <div class="form-hint">Supports **bold**, *italic*, \`code\`, and \`\`\`code blocks\`\`\`</div> 3494 3642 </div> 3495 3643 3496 3644 <div class="form-group">
+331
docs/plans/2025-12-19-markdown-facets.md
··· 1 + # Markdown Facets for Bug Descriptions 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add markdown formatting (bold, italic, code, URLs) to bug descriptions and steps to reproduce using ATProto-style facets. 6 + 7 + **Architecture:** Define custom richtext facet lexicon, parse markdown syntax into facets on input, strip syntax from stored text, render facets as formatted HTML on display. Follows Leaflet's `pub.leaflet.richtext.facet` pattern. 8 + 9 + **Tech Stack:** ATProto lexicons (JSON), vanilla JS, regex parsing, byte offsets. 10 + 11 + --- 12 + 13 + ### Task 0: Create Custom Richtext Facet Lexicon 14 + 15 + **Files:** 16 + - Create: `lexicons/network/slices/tools/richtext/facet.json` 17 + - Modify: `lexicons/network/slices/tools/bug.json` 18 + 19 + **Step 1: Create the richtext directory** 20 + 21 + ```bash 22 + mkdir -p lexicons/network/slices/tools/richtext 23 + ``` 24 + 25 + **Step 2: Create the facet lexicon** 26 + 27 + Create `lexicons/network/slices/tools/richtext/facet.json`: 28 + 29 + ```json 30 + { 31 + "lexicon": 1, 32 + "id": "network.slices.tools.richtext.facet", 33 + "defs": { 34 + "main": { 35 + "type": "object", 36 + "description": "Annotation of a sub-string within rich text.", 37 + "required": ["index", "features"], 38 + "properties": { 39 + "index": { "type": "ref", "ref": "#byteSlice" }, 40 + "features": { 41 + "type": "array", 42 + "items": { 43 + "type": "union", 44 + "refs": ["#link", "#bold", "#italic", "#code"] 45 + } 46 + } 47 + } 48 + }, 49 + "byteSlice": { 50 + "type": "object", 51 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text.", 52 + "required": ["byteStart", "byteEnd"], 53 + "properties": { 54 + "byteStart": { "type": "integer", "minimum": 0 }, 55 + "byteEnd": { "type": "integer", "minimum": 0 } 56 + } 57 + }, 58 + "link": { 59 + "type": "object", 60 + "description": "Facet feature for a URL.", 61 + "required": ["uri"], 62 + "properties": { 63 + "uri": { "type": "string", "format": "uri" } 64 + } 65 + }, 66 + "bold": { 67 + "type": "object", 68 + "description": "Facet feature for bold text.", 69 + "required": [], 70 + "properties": {} 71 + }, 72 + "italic": { 73 + "type": "object", 74 + "description": "Facet feature for italic text.", 75 + "required": [], 76 + "properties": {} 77 + }, 78 + "code": { 79 + "type": "object", 80 + "description": "Facet feature for inline code.", 81 + "required": [], 82 + "properties": {} 83 + } 84 + } 85 + } 86 + ``` 87 + 88 + **Step 3: Update bug.json to use new facet** 89 + 90 + In `lexicons/network/slices/tools/bug.json`, change both facet references from `app.bsky.richtext.facet` to `network.slices.tools.richtext.facet`: 91 + 92 + Line 28: `"ref": "app.bsky.richtext.facet"` → `"ref": "network.slices.tools.richtext.facet"` 93 + Line 38: `"ref": "app.bsky.richtext.facet"` → `"ref": "network.slices.tools.richtext.facet"` 94 + 95 + **Step 4: Commit** 96 + 97 + ```bash 98 + git add lexicons/ 99 + git commit -m "feat(lexicons): add richtext facet with bold/italic/code support" 100 + ``` 101 + 102 + --- 103 + 104 + ### Task 1: Add CSS for Formatting Facets 105 + 106 + **Files:** 107 + - Modify: `bugs.html:283-290` (after existing `.facet-link` styles) 108 + 109 + **Step 1: Add the CSS classes** 110 + 111 + Add after line 290 (after `.facet-link:hover`): 112 + 113 + ```css 114 + .facet-bold { 115 + font-weight: 600; 116 + } 117 + 118 + .facet-italic { 119 + font-style: italic; 120 + } 121 + 122 + .facet-code { 123 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 124 + background: var(--bg-hover); 125 + padding: 0.125rem 0.25rem; 126 + border-radius: 0.25rem; 127 + font-size: 0.875em; 128 + } 129 + 130 + .form-hint { 131 + font-size: 0.75rem; 132 + color: var(--text-secondary); 133 + margin-top: 0.25rem; 134 + } 135 + ``` 136 + 137 + **Step 2: Commit** 138 + 139 + ```bash 140 + git add bugs.html 141 + git commit -m "feat(bugs): add CSS for bold/italic/code facets" 142 + ``` 143 + 144 + --- 145 + 146 + ### Task 2: Rewrite parseFacets to Handle Markdown 147 + 148 + **Files:** 149 + - Modify: `bugs.html:1337-1362` (replace entire `parseFacets` function) 150 + 151 + **Step 1: Replace the function** 152 + 153 + Replace the existing `parseFacets` function (lines 1337-1362) with: 154 + 155 + ```javascript 156 + function parseFacets(text) { 157 + if (!text) return { text, facets: null }; 158 + 159 + const facets = []; 160 + let processedText = text; 161 + 162 + // Process markdown patterns (order matters: bold before italic) 163 + const patterns = [ 164 + { regex: /\*\*(.+?)\*\*/g, type: 'bold', delimLen: 2 }, 165 + { regex: /\*(.+?)\*/g, type: 'italic', delimLen: 1 }, 166 + { regex: /_(.+?)_/g, type: 'italic', delimLen: 1 }, 167 + { regex: /`(.+?)`/g, type: 'code', delimLen: 1 }, 168 + ]; 169 + 170 + for (const { regex, type } of patterns) { 171 + let match; 172 + const newRegex = new RegExp(regex.source, regex.flags); 173 + 174 + while ((match = newRegex.exec(processedText)) !== null) { 175 + const fullMatch = match[0]; 176 + const content = match[1]; 177 + const matchStart = match.index; 178 + 179 + // Remove delimiters from text 180 + const before = processedText.slice(0, matchStart); 181 + const after = processedText.slice(matchStart + fullMatch.length); 182 + processedText = before + content + after; 183 + 184 + // Calculate byte positions in the NEW processed text 185 + const byteStart = new TextEncoder().encode(before).length; 186 + const byteEnd = byteStart + new TextEncoder().encode(content).length; 187 + 188 + facets.push({ 189 + index: { byteStart, byteEnd }, 190 + features: [{ $type: `network.slices.tools.richtext.facet#${type}` }], 191 + }); 192 + 193 + // Reset regex to continue from adjusted position 194 + newRegex.lastIndex = matchStart + content.length; 195 + } 196 + } 197 + 198 + // Process URLs (no stripping needed) 199 + const urlRegex = /https?:\/\/[^\s<>"\]\)]+/gi; 200 + let match; 201 + while ((match = urlRegex.exec(processedText)) !== null) { 202 + const url = match[0]; 203 + const byteStart = new TextEncoder().encode(processedText.slice(0, match.index)).length; 204 + const byteEnd = byteStart + new TextEncoder().encode(url).length; 205 + 206 + facets.push({ 207 + index: { byteStart, byteEnd }, 208 + features: [{ $type: "network.slices.tools.richtext.facet#link", uri: url }], 209 + }); 210 + } 211 + 212 + // Sort facets by position 213 + facets.sort((a, b) => a.index.byteStart - b.index.byteStart); 214 + 215 + return { 216 + text: processedText, 217 + facets: facets.length > 0 ? facets : null, 218 + }; 219 + } 220 + ``` 221 + 222 + **Step 2: Test manually in browser** 223 + 224 + Open bugs.html, open console, run: 225 + ```javascript 226 + parseFacets("This is **bold** and *italic* and `code` with https://example.com") 227 + ``` 228 + 229 + Verify output has stripped text and 4 facets. 230 + 231 + **Step 3: Commit** 232 + 233 + ```bash 234 + git add bugs.html 235 + git commit -m "feat(bugs): parse markdown into facets with syntax stripping" 236 + ``` 237 + 238 + --- 239 + 240 + ### Task 3: Update renderFacetedText for New Facet Types 241 + 242 + **Files:** 243 + - Modify: `bugs.html:1391-1397` (facet rendering section in `renderFacetedText`) 244 + 245 + **Step 1: Replace the facet rendering logic** 246 + 247 + Find lines 1390-1397 and replace with: 248 + 249 + ```javascript 250 + const facetText = decoder.decode(bytes.slice(facet.index.byteStart, facet.index.byteEnd)); 251 + const link = facet.features.find((f) => f.uri); 252 + const bold = facet.features.find((f) => f.$type?.endsWith('#bold')); 253 + const italic = facet.features.find((f) => f.$type?.endsWith('#italic')); 254 + const code = facet.features.find((f) => f.$type?.endsWith('#code')); 255 + 256 + if (link) { 257 + result += `<a href="${esc(link.uri)}" target="_blank" rel="noopener noreferrer" class="facet-link">${esc(facetText)}</a>`; 258 + } else if (code) { 259 + result += `<code class="facet-code">${esc(facetText)}</code>`; 260 + } else if (bold && italic) { 261 + result += `<strong class="facet-bold"><em class="facet-italic">${esc(facetText)}</em></strong>`; 262 + } else if (bold) { 263 + result += `<strong class="facet-bold">${esc(facetText)}</strong>`; 264 + } else if (italic) { 265 + result += `<em class="facet-italic">${esc(facetText)}</em>`; 266 + } else { 267 + result += esc(facetText); 268 + } 269 + ``` 270 + 271 + **Step 2: Test manually in browser** 272 + 273 + Create a bug with markdown. View it. Verify formatting renders. 274 + 275 + **Step 3: Commit** 276 + 277 + ```bash 278 + git add bugs.html 279 + git commit -m "feat(bugs): render bold/italic/code facets as HTML" 280 + ``` 281 + 282 + --- 283 + 284 + ### Task 4: Add Formatting Hints to Forms 285 + 286 + **Files:** 287 + - Modify: `bugs.html:3333` (after description textarea in create form) 288 + - Modify: `bugs.html:3338` (after steps textarea in create form) 289 + - Modify: `bugs.html:3488` (after description textarea in edit form) 290 + - Modify: `bugs.html:3493` (after steps textarea in edit form) 291 + 292 + **Step 1: Add hint after each textarea** 293 + 294 + After each `</textarea>` for description and steps, add: 295 + ```html 296 + <div class="form-hint">Supports **bold**, *italic*, and `code`</div> 297 + ``` 298 + 299 + **Step 2: Commit** 300 + 301 + ```bash 302 + git add bugs.html 303 + git commit -m "feat(bugs): add markdown formatting hints to forms" 304 + ``` 305 + 306 + --- 307 + 308 + ### Task 5: Final Testing 309 + 310 + **Step 1: Test the full flow** 311 + 312 + 1. Open bugs.html in browser 313 + 2. Create a new bug with: 314 + - Title: "Test markdown formatting" 315 + - Description: `The **login button** is *broken* when using \`OAuth\` flow. See https://example.com` 316 + - Steps: `1. Go to **Settings**\n2. Click *OAuth* tab` 317 + 3. Submit and verify formatted display 318 + 4. Edit the bug, verify formatting persists 319 + 5. Check existing bugs still render (backwards compatibility with old `app.bsky.richtext.facet#link`) 320 + 321 + **Step 2: Commit any fixes if needed** 322 + 323 + --- 324 + 325 + ## Summary 326 + 327 + After completing all tasks: 328 + 1. New lexicon `network.slices.tools.richtext.facet` with bold/italic/code/link 329 + 2. `bugs.html` parses markdown → facets, strips syntax 330 + 3. Renders facets as formatted HTML 331 + 4. Shows formatting hints in forms
+74
lexicons/app/bsky/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.profile", 4 + "defs": { 5 + "main": { 6 + "key": "literal:self", 7 + "type": "record", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "avatar": { 12 + "type": "blob", 13 + "accept": [ 14 + "image/png", 15 + "image/jpeg" 16 + ], 17 + "maxSize": 1000000, 18 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 19 + }, 20 + "banner": { 21 + "type": "blob", 22 + "accept": [ 23 + "image/png", 24 + "image/jpeg" 25 + ], 26 + "maxSize": 1000000, 27 + "description": "Larger horizontal image to display behind profile view." 28 + }, 29 + "labels": { 30 + "refs": [ 31 + "com.atproto.label.defs#selfLabels" 32 + ], 33 + "type": "union", 34 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 35 + }, 36 + "website": { 37 + "type": "string", 38 + "format": "uri" 39 + }, 40 + "pronouns": { 41 + "type": "string", 42 + "maxLength": 200, 43 + "description": "Free-form pronouns text.", 44 + "maxGraphemes": 20 45 + }, 46 + "createdAt": { 47 + "type": "string", 48 + "format": "datetime" 49 + }, 50 + "pinnedPost": { 51 + "ref": "com.atproto.repo.strongRef", 52 + "type": "ref" 53 + }, 54 + "description": { 55 + "type": "string", 56 + "maxLength": 2560, 57 + "description": "Free-form profile description text.", 58 + "maxGraphemes": 256 59 + }, 60 + "displayName": { 61 + "type": "string", 62 + "maxLength": 640, 63 + "maxGraphemes": 64 64 + }, 65 + "joinedViaStarterPack": { 66 + "ref": "com.atproto.repo.strongRef", 67 + "type": "ref" 68 + } 69 + } 70 + }, 71 + "description": "A declaration of a Bluesky account profile." 72 + } 73 + } 74 + }
+89
lexicons/app/bsky/richtext/facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.richtext.facet", 4 + "defs": { 5 + "tag": { 6 + "type": "object", 7 + "required": [ 8 + "tag" 9 + ], 10 + "properties": { 11 + "tag": { 12 + "type": "string", 13 + "maxLength": 640, 14 + "maxGraphemes": 64 15 + } 16 + }, 17 + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags')." 18 + }, 19 + "link": { 20 + "type": "object", 21 + "required": [ 22 + "uri" 23 + ], 24 + "properties": { 25 + "uri": { 26 + "type": "string", 27 + "format": "uri" 28 + } 29 + }, 30 + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL." 31 + }, 32 + "main": { 33 + "type": "object", 34 + "required": [ 35 + "index", 36 + "features" 37 + ], 38 + "properties": { 39 + "index": { 40 + "ref": "#byteSlice", 41 + "type": "ref" 42 + }, 43 + "features": { 44 + "type": "array", 45 + "items": { 46 + "refs": [ 47 + "#mention", 48 + "#link", 49 + "#tag" 50 + ], 51 + "type": "union" 52 + } 53 + } 54 + }, 55 + "description": "Annotation of a sub-string within rich text." 56 + }, 57 + "mention": { 58 + "type": "object", 59 + "required": [ 60 + "did" 61 + ], 62 + "properties": { 63 + "did": { 64 + "type": "string", 65 + "format": "did" 66 + } 67 + }, 68 + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID." 69 + }, 70 + "byteSlice": { 71 + "type": "object", 72 + "required": [ 73 + "byteStart", 74 + "byteEnd" 75 + ], 76 + "properties": { 77 + "byteEnd": { 78 + "type": "integer", 79 + "minimum": 0 80 + }, 81 + "byteStart": { 82 + "type": "integer", 83 + "minimum": 0 84 + } 85 + }, 86 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets." 87 + } 88 + } 89 + }
+192
lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "label": { 6 + "type": "object", 7 + "required": [ 8 + "src", 9 + "uri", 10 + "val", 11 + "cts" 12 + ], 13 + "properties": { 14 + "cid": { 15 + "type": "string", 16 + "format": "cid", 17 + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." 18 + }, 19 + "cts": { 20 + "type": "string", 21 + "format": "datetime", 22 + "description": "Timestamp when this label was created." 23 + }, 24 + "exp": { 25 + "type": "string", 26 + "format": "datetime", 27 + "description": "Timestamp at which this label expires (no longer applies)." 28 + }, 29 + "neg": { 30 + "type": "boolean", 31 + "description": "If true, this is a negation label, overwriting a previous label." 32 + }, 33 + "sig": { 34 + "type": "bytes", 35 + "description": "Signature of dag-cbor encoded label." 36 + }, 37 + "src": { 38 + "type": "string", 39 + "format": "did", 40 + "description": "DID of the actor who created this label." 41 + }, 42 + "uri": { 43 + "type": "string", 44 + "format": "uri", 45 + "description": "AT URI of the record, repository (account), or other resource that this label applies to." 46 + }, 47 + "val": { 48 + "type": "string", 49 + "maxLength": 128, 50 + "description": "The short string name of the value or type of this label." 51 + }, 52 + "ver": { 53 + "type": "integer", 54 + "description": "The AT Protocol version of the label object." 55 + } 56 + }, 57 + "description": "Metadata tag on an atproto resource (eg, repo or record)." 58 + }, 59 + "selfLabel": { 60 + "type": "object", 61 + "required": [ 62 + "val" 63 + ], 64 + "properties": { 65 + "val": { 66 + "type": "string", 67 + "maxLength": 128, 68 + "description": "The short string name of the value or type of this label." 69 + } 70 + }, 71 + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel." 72 + }, 73 + "labelValue": { 74 + "type": "string", 75 + "knownValues": [ 76 + "!hide", 77 + "!no-promote", 78 + "!warn", 79 + "!no-unauthenticated", 80 + "dmca-violation", 81 + "doxxing", 82 + "porn", 83 + "sexual", 84 + "nudity", 85 + "nsfl", 86 + "gore" 87 + ] 88 + }, 89 + "selfLabels": { 90 + "type": "object", 91 + "required": [ 92 + "values" 93 + ], 94 + "properties": { 95 + "values": { 96 + "type": "array", 97 + "items": { 98 + "ref": "#selfLabel", 99 + "type": "ref" 100 + }, 101 + "maxLength": 10 102 + } 103 + }, 104 + "description": "Metadata tags on an atproto record, published by the author within the record." 105 + }, 106 + "labelValueDefinition": { 107 + "type": "object", 108 + "required": [ 109 + "identifier", 110 + "severity", 111 + "blurs", 112 + "locales" 113 + ], 114 + "properties": { 115 + "blurs": { 116 + "type": "string", 117 + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 118 + "knownValues": [ 119 + "content", 120 + "media", 121 + "none" 122 + ] 123 + }, 124 + "locales": { 125 + "type": "array", 126 + "items": { 127 + "ref": "#labelValueDefinitionStrings", 128 + "type": "ref" 129 + } 130 + }, 131 + "severity": { 132 + "type": "string", 133 + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 134 + "knownValues": [ 135 + "inform", 136 + "alert", 137 + "none" 138 + ] 139 + }, 140 + "adultOnly": { 141 + "type": "boolean", 142 + "description": "Does the user need to have adult content enabled in order to configure this label?" 143 + }, 144 + "identifier": { 145 + "type": "string", 146 + "maxLength": 100, 147 + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 148 + "maxGraphemes": 100 149 + }, 150 + "defaultSetting": { 151 + "type": "string", 152 + "default": "warn", 153 + "description": "The default setting for this label.", 154 + "knownValues": [ 155 + "ignore", 156 + "warn", 157 + "hide" 158 + ] 159 + } 160 + }, 161 + "description": "Declares a label value and its expected interpretations and behaviors." 162 + }, 163 + "labelValueDefinitionStrings": { 164 + "type": "object", 165 + "required": [ 166 + "lang", 167 + "name", 168 + "description" 169 + ], 170 + "properties": { 171 + "lang": { 172 + "type": "string", 173 + "format": "language", 174 + "description": "The code of the language these strings are written in." 175 + }, 176 + "name": { 177 + "type": "string", 178 + "maxLength": 640, 179 + "description": "A short human-readable name for the label.", 180 + "maxGraphemes": 64 181 + }, 182 + "description": { 183 + "type": "string", 184 + "maxLength": 100000, 185 + "description": "A longer description of what the label means and why it might be applied.", 186 + "maxGraphemes": 10000 187 + } 188 + }, 189 + "description": "Strings which describe the label in the UI, localized into a specific language." 190 + } 191 + } 192 + }
+24
lexicons/com/atproto/repo/strongRef.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.strongRef", 4 + "description": "A URI with a content-hash fingerprint.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": [ 9 + "uri", 10 + "cid" 11 + ], 12 + "properties": { 13 + "cid": { 14 + "type": "string", 15 + "format": "cid" 16 + }, 17 + "uri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + } 21 + } 22 + } 23 + } 24 + }
+2 -2
lexicons/network/slices/tools/bug.json
··· 25 25 }, 26 26 "descriptionFacets": { 27 27 "type": "array", 28 - "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }, 28 + "items": { "type": "ref", "ref": "network.slices.tools.richtext.facet" }, 29 29 "description": "Annotations of description (mentions and links)" 30 30 }, 31 31 "stepsToReproduce": { ··· 35 35 }, 36 36 "stepsToReproduceFacets": { 37 37 "type": "array", 38 - "items": { "type": "ref", "ref": "app.bsky.richtext.facet" }, 38 + "items": { "type": "ref", "ref": "network.slices.tools.richtext.facet" }, 39 39 "description": "Annotations of steps to reproduce (mentions and links)" 40 40 }, 41 41 "severity": {
+67
lexicons/network/slices/tools/richtext/facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "network.slices.tools.richtext.facet", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "Annotation of a sub-string within rich text.", 8 + "required": ["index", "features"], 9 + "properties": { 10 + "index": { "type": "ref", "ref": "#byteSlice" }, 11 + "features": { 12 + "type": "array", 13 + "items": { 14 + "type": "union", 15 + "refs": ["#link", "#bold", "#italic", "#code", "#codeBlock"] 16 + } 17 + } 18 + } 19 + }, 20 + "byteSlice": { 21 + "type": "object", 22 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text.", 23 + "required": ["byteStart", "byteEnd"], 24 + "properties": { 25 + "byteStart": { "type": "integer", "minimum": 0 }, 26 + "byteEnd": { "type": "integer", "minimum": 0 } 27 + } 28 + }, 29 + "link": { 30 + "type": "object", 31 + "description": "Facet feature for a URL.", 32 + "required": ["uri"], 33 + "properties": { 34 + "uri": { "type": "string", "format": "uri" } 35 + } 36 + }, 37 + "bold": { 38 + "type": "object", 39 + "description": "Facet feature for bold text.", 40 + "required": [], 41 + "properties": {} 42 + }, 43 + "italic": { 44 + "type": "object", 45 + "description": "Facet feature for italic text.", 46 + "required": [], 47 + "properties": {} 48 + }, 49 + "code": { 50 + "type": "object", 51 + "description": "Facet feature for inline code.", 52 + "required": [], 53 + "properties": {} 54 + }, 55 + "codeBlock": { 56 + "type": "object", 57 + "description": "Facet feature for fenced code blocks.", 58 + "required": [], 59 + "properties": { 60 + "lang": { 61 + "type": "string", 62 + "description": "Optional language identifier for syntax highlighting." 63 + } 64 + } 65 + } 66 + } 67 + }
+65
lexicons/sh/tangled/repo.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": ["name", "knot", "createdAt"], 11 + "properties": { 12 + "name": { 13 + "type": "string", 14 + "description": "name of the repo" 15 + }, 16 + "knot": { 17 + "type": "string", 18 + "description": "knot where the repo was created" 19 + }, 20 + "spindle": { 21 + "type": "string", 22 + "description": "CI runner to send jobs to and receive results from" 23 + }, 24 + "description": { 25 + "type": "string", 26 + "minGraphemes": 1, 27 + "maxGraphemes": 140 28 + }, 29 + "website": { 30 + "type": "string", 31 + "format": "uri", 32 + "description": "Any URI related to the repo" 33 + }, 34 + "topics": { 35 + "type": "array", 36 + "description": "Topics related to the repo", 37 + "items": { 38 + "type": "string", 39 + "minLength": 1, 40 + "maxLength": 50 41 + }, 42 + "maxLength": 50 43 + }, 44 + "source": { 45 + "type": "string", 46 + "format": "uri", 47 + "description": "source of the repo" 48 + }, 49 + "labels": { 50 + "type": "array", 51 + "description": "List of labels that this repo subscribes to", 52 + "items": { 53 + "type": "string", 54 + "format": "at-uri" 55 + } 56 + }, 57 + "createdAt": { 58 + "type": "string", 59 + "format": "datetime" 60 + } 61 + } 62 + } 63 + } 64 + } 65 + }
+30
lexicons/sh/tangled/repo/issue.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo.issue", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": ["repo", "title", "createdAt"], 11 + "properties": { 12 + "repo": { 13 + "type": "string", 14 + "format": "at-uri" 15 + }, 16 + "title": { 17 + "type": "string" 18 + }, 19 + "body": { 20 + "type": "string" 21 + }, 22 + "createdAt": { 23 + "type": "string", 24 + "format": "datetime" 25 + } 26 + } 27 + } 28 + } 29 + } 30 + }