this repo has no description
1---
2import type { StandardBlock, Facet } from '../../lib/types';
3import { escapeHtml } from '../../lib/render';
4
5interface Props {
6 blocks: StandardBlock[];
7}
8
9const { blocks } = Astro.props;
10
11function renderBlockFacets(text: string, facets?: Facet[]): string {
12 if (!facets?.length) return escapeHtml(text);
13 const encoder = new TextEncoder();
14 const decoder = new TextDecoder();
15 const bytes = encoder.encode(text);
16 const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart);
17 let result = '';
18 let lastEnd = 0;
19 for (const facet of sorted) {
20 const { byteStart, byteEnd } = facet.index;
21 result += escapeHtml(decoder.decode(bytes.slice(lastEnd, byteStart)));
22 const facetText = escapeHtml(decoder.decode(bytes.slice(byteStart, byteEnd)));
23 const feature = facet.features?.[0];
24 if (feature?.$type?.includes('#link') && feature.uri) {
25 result += `<a href="${escapeHtml(feature.uri)}" target="_blank" rel="noopener">${facetText}</a>`;
26 } else if (feature?.$type?.includes('#code')) {
27 result += `<code>${facetText}</code>`;
28 } else if (feature?.$type?.includes('#bold')) {
29 result += `<strong>${facetText}</strong>`;
30 } else if (feature?.$type?.includes('#italic')) {
31 result += `<em>${facetText}</em>`;
32 } else {
33 result += facetText;
34 }
35 lastEnd = byteEnd;
36 }
37 result += escapeHtml(decoder.decode(bytes.slice(lastEnd)));
38 return result;
39}
40
41function renderBlock(block: StandardBlock): string {
42 const type = block.$type || '';
43
44 if (type.includes('text')) {
45 const text = block.text || block.plaintext || '';
46 if (!text.trim()) return '';
47 return `<p>${renderBlockFacets(text, block.facets)}</p>`;
48 }
49
50 if (type.includes('header') || type.includes('heading')) {
51 const text = block.text || block.plaintext || '';
52 const level = block.level || 2;
53 const tag = level <= 3 ? `h${level}` : 'h4';
54 return `<${tag}>${escapeHtml(text)}</${tag}>`;
55 }
56
57 if (type.includes('code')) {
58 const text = block.text || block.plaintext || '';
59 const lang = block.language || '';
60 return `<pre><code class="lang-${escapeHtml(lang)}">${escapeHtml(text)}</code></pre>`;
61 }
62
63 if (type.includes('image')) {
64 const alt = block.alt || '';
65 // Skip images for now — blob rendering is complex
66 return alt ? `<p class="image-placeholder">[Image: ${escapeHtml(alt)}]</p>` : '';
67 }
68
69 if (type.includes('list') || type.includes('List')) {
70 const children = block.children || block.items || [];
71 const items = children.map((child: any) => {
72 const content = child.content || child;
73 const text = content.text || content.plaintext || '';
74 return `<li>${renderBlockFacets(text, content.facets)}</li>`;
75 }).join('');
76 return `<ul>${items}</ul>`;
77 }
78
79 return '';
80}
81---
82
83<div class="article-content">
84 {blocks.map(block => {
85 // Handle wrapped blocks (pub.leaflet style)
86 const innerBlock = block.block || block;
87 const html = renderBlock(innerBlock);
88 return html ? <Fragment set:html={html} /> : null;
89 })}
90</div>
91
92<style>
93 .article-content {
94 font-size: 0.95rem;
95 line-height: 1.7;
96 }
97 .article-content :global(p) { margin-bottom: 1em; }
98 .article-content :global(h2) {
99 font-size: 1.6rem; font-weight: 900; text-transform: uppercase;
100 letter-spacing: 0.03em; margin: 1.5em 0 0.5em; color: var(--text);
101 border-bottom: 3px solid var(--black); padding-bottom: 0.3em;
102 }
103 .article-content :global(h3) {
104 font-size: 1.15rem; font-weight: 800; text-transform: uppercase;
105 margin: 1.2em 0 0.4em; color: var(--text);
106 }
107 .article-content :global(pre) {
108 background: var(--black); color: #fff; padding: 16px;
109 border: 3px solid var(--black); overflow-x: auto; margin: 1em 0;
110 font-size: 0.82rem; line-height: 1.5;
111 }
112 .article-content :global(code) {
113 font-family: 'SF Mono', 'Fira Code', monospace;
114 font-size: 0.85em;
115 }
116 .article-content :global(p code) {
117 background: rgba(0, 0, 255, 0.08);
118 padding: 2px 5px;
119 color: var(--text);
120 border: 1px solid rgba(0, 0, 0, 0.15);
121 }
122 .article-content :global(ul) {
123 margin: 0.5em 0 1em 1.5em;
124 }
125 .article-content :global(li) { margin-bottom: 0.3em; }
126 .article-content :global(a) {
127 color: var(--blue);
128 border-bottom: 2px solid var(--blue);
129 }
130 .article-content :global(a:hover) {
131 background: var(--yellow);
132 }
133 .image-placeholder { color: var(--text-muted); font-weight: 700; text-transform: uppercase; }
134</style>