@rettangoli/sites 0.2.7 → 1.0.0-rc10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Adapted from https://github.com/antfu/markdown-it-async
3
+ */
4
+
5
+ import MarkdownIt from 'markdown-it';
6
+
7
+ const placeholder = (id, code) => `<pre><!--::markdown-it-async::${id}::--><code>${code}</code></pre>`;
8
+ const placeholderRe = /<pre><!--::markdown-it-async::(\w+)::--><code>[\s\S]*?<\/code><\/pre>/g;
9
+ const wrappedSet = new WeakSet();
10
+
11
+ function randStr() {
12
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
13
+ }
14
+
15
+ function escapeHtml(unsafe) {
16
+ return unsafe
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#039;');
22
+ }
23
+
24
+ function wrapHighlight(highlight, map) {
25
+ if (!highlight) {
26
+ return undefined;
27
+ }
28
+
29
+ if (wrappedSet.has(highlight)) {
30
+ return highlight;
31
+ }
32
+
33
+ const wrapped = (str, lang, attrs) => {
34
+ const result = highlight(str, lang, attrs);
35
+ if (typeof result === 'string') {
36
+ return result;
37
+ }
38
+
39
+ const id = randStr();
40
+ map.set(id, [result, str, lang, attrs]);
41
+ const escapedCode = escapeHtml(str.endsWith('\n') ? str.slice(0, -1) : str);
42
+ return placeholder(id, escapedCode);
43
+ };
44
+
45
+ wrappedSet.add(wrapped);
46
+ return wrapped;
47
+ }
48
+
49
+ async function replaceAsync(string, searchValue, replacer) {
50
+ const values = [];
51
+ String.prototype.replace.call(string, searchValue, (...args) => {
52
+ values.push(replacer(...args));
53
+ return '';
54
+ });
55
+
56
+ const resolvedValues = await Promise.all(values);
57
+ return String.prototype.replace.call(string, searchValue, () => resolvedValues.shift() || '');
58
+ }
59
+
60
+ export class MarkdownItAsync extends MarkdownIt {
61
+ constructor(...args) {
62
+ const options = args.length === 2 ? args[1] : args[0];
63
+ const map = new Map();
64
+
65
+ if (options && 'highlight' in options) {
66
+ options.highlight = wrapHighlight(options.highlight, map);
67
+ }
68
+
69
+ super(...args);
70
+ this.placeholderMap = map;
71
+ this.disableWarn = false;
72
+ }
73
+
74
+ render(src, env) {
75
+ if (this.options.warnOnSyncRender && !this.disableWarn) {
76
+ console.warn('[markdown-it-async] Please use `md.renderAsync` instead of `md.render`');
77
+ }
78
+ return super.render(src, env);
79
+ }
80
+
81
+ async renderAsync(src, env) {
82
+ this.options.highlight = wrapHighlight(this.options.highlight, this.placeholderMap);
83
+ this.disableWarn = true;
84
+ const result = this.render(src, env);
85
+ this.disableWarn = false;
86
+
87
+ return replaceAsync(result, placeholderRe, async (_match, id) => {
88
+ const item = this.placeholderMap.get(id);
89
+ if (!item) {
90
+ throw new Error(`Unknown highlight placeholder id: ${id}`);
91
+ }
92
+ const [promise] = item;
93
+ const highlighted = (await promise) || '';
94
+ this.placeholderMap.delete(id);
95
+ return highlighted;
96
+ });
97
+ }
98
+ }
99
+
100
+ export function createMarkdownItAsync(...args) {
101
+ return new MarkdownItAsync(...args);
102
+ }
103
+
104
+ export default createMarkdownItAsync;
@@ -1,22 +1,161 @@
1
- // Custom slug generation function
2
- function slugify(text) {
3
- return text
1
+ import { codeToHtml } from 'shiki';
2
+ import { createMarkdownItAsync } from './markdownItAsync.js';
3
+
4
+ function resolveHeadingAnchorOptions(input) {
5
+ if (input === false) {
6
+ return { enabled: false, slugMode: 'unicode', wrap: true, fallback: 'section' };
7
+ }
8
+
9
+ if (input === true || input == null) {
10
+ return { enabled: true, slugMode: 'unicode', wrap: true, fallback: 'section' };
11
+ }
12
+
13
+ const candidate = typeof input === 'object' && !Array.isArray(input) ? input : {};
14
+ const fallback = typeof candidate.fallback === 'string' && candidate.fallback.trim()
15
+ ? candidate.fallback.trim()
16
+ : 'section';
17
+
18
+ return {
19
+ enabled: candidate.enabled !== undefined ? !!candidate.enabled : true,
20
+ slugMode: candidate.slugMode === 'ascii' ? 'ascii' : 'unicode',
21
+ wrap: candidate.wrap !== undefined ? !!candidate.wrap : true,
22
+ fallback
23
+ };
24
+ }
25
+
26
+ function resolveCodePreviewOptions(input) {
27
+ if (input === false || input == null) {
28
+ return { enabled: false, showSource: true, theme: undefined };
29
+ }
30
+
31
+ if (input === true) {
32
+ return { enabled: true, showSource: true, theme: undefined };
33
+ }
34
+
35
+ if (typeof input === 'object' && !Array.isArray(input)) {
36
+ const theme = typeof input.theme === 'string' && input.theme.trim()
37
+ ? input.theme.trim()
38
+ : undefined;
39
+ return {
40
+ enabled: input.enabled !== undefined ? !!input.enabled : true,
41
+ showSource: input.showSource !== undefined ? !!input.showSource : true,
42
+ theme
43
+ };
44
+ }
45
+
46
+ return { enabled: false, showSource: true, theme: undefined };
47
+ }
48
+
49
+ function hasCodePreviewAttribute(attrs) {
50
+ if (typeof attrs !== 'string') {
51
+ return false;
52
+ }
53
+ return attrs
54
+ .split(/\s+/)
55
+ .map((item) => item.trim())
56
+ .filter(Boolean)
57
+ .includes('codePreview');
58
+ }
59
+
60
+ function renderCodePreviewLayout(code, highlightedCode, showSource = true) {
61
+ if (!showSource) {
62
+ return `
63
+ <rtgl-view class="rtgl-code-preview" w="f" bw="xs" br="md">
64
+ <rtgl-view w="f" d="h">
65
+ ${highlightedCode}
66
+ </rtgl-view>
67
+ </rtgl-view>`;
68
+ }
69
+
70
+ return `
71
+ <rtgl-view class="rtgl-code-preview" w="f" bw="xs" br="md">
72
+ <rtgl-view w="f" p="lg">
73
+ ${code}
74
+ </rtgl-view>
75
+ <rtgl-view h="1" w="f" bgc="bo"></rtgl-view>
76
+ <rtgl-view w="f" d="h">
77
+ ${highlightedCode}
78
+ </rtgl-view>
79
+ </rtgl-view>`;
80
+ }
81
+
82
+ function slugify(text, { slugMode = 'unicode', fallback = 'section' } = {}) {
83
+ const normalized = String(text || '')
4
84
  .toLowerCase()
85
+ .normalize('NFKD')
86
+ .replace(/[\u0300-\u036f]/g, '')
5
87
  .trim()
6
- .replace(/[^\w\s-]/g, '') // Remove non-word chars (except spaces and hyphens)
88
+ .replace(
89
+ slugMode === 'ascii' ? /[^a-z0-9\s-]/g : /[^\p{Letter}\p{Number}\s-]/gu,
90
+ ''
91
+ )
7
92
  .replace(/\s+/g, '-') // Replace spaces with hyphens
8
93
  .replace(/--+/g, '-') // Replace multiple hyphens with single hyphen
9
94
  .replace(/^-+/, '') // Remove leading hyphens
10
95
  .replace(/-+$/, '') // Remove trailing hyphens
96
+
97
+ return normalized || fallback;
11
98
  }
12
99
 
13
- export default function configureMarkdown(markdownit) {
14
- const md = markdownit({
15
- html: true,
16
- linkify: true,
17
- typographer: false
100
+ export function createRtglMarkdown(_markdownit, options = {}) {
101
+ const {
102
+ preset = 'default',
103
+ html = true,
104
+ xhtmlOut = false,
105
+ linkify = true,
106
+ typographer = false,
107
+ breaks = false,
108
+ langPrefix = 'language-',
109
+ quotes = '\u201c\u201d\u2018\u2019',
110
+ maxNesting = 100,
111
+ shiki = {},
112
+ headingAnchors = true,
113
+ codePreview = false
114
+ } = options;
115
+ const headingAnchorOptions = resolveHeadingAnchorOptions(headingAnchors);
116
+ const codePreviewOptions = resolveCodePreviewOptions(codePreview);
117
+
118
+ const shikiEnabled = typeof shiki.enabled === 'boolean' ? shiki.enabled : true;
119
+ const shikiTheme = typeof shiki.theme === 'string' ? shiki.theme : 'slack-dark';
120
+
121
+ const md = createMarkdownItAsync(preset, {
122
+ html,
123
+ xhtmlOut,
124
+ linkify,
125
+ typographer,
126
+ breaks,
127
+ langPrefix,
128
+ quotes,
129
+ maxNesting,
130
+ ...(shikiEnabled
131
+ ? {
132
+ async highlight(code, lang, attrs) {
133
+ try {
134
+ const isCodePreview = codePreviewOptions.enabled && hasCodePreviewAttribute(attrs);
135
+ const highlightTheme = isCodePreview
136
+ ? (codePreviewOptions.theme || shikiTheme)
137
+ : shikiTheme;
138
+ const highlightedCode = await codeToHtml(code, {
139
+ lang: lang || 'text',
140
+ theme: highlightTheme
141
+ });
142
+ if (isCodePreview) {
143
+ return renderCodePreviewLayout(code, highlightedCode, codePreviewOptions.showSource);
144
+ }
145
+ return highlightedCode;
146
+ } catch {
147
+ return '';
148
+ }
149
+ }
150
+ }
151
+ : {}),
152
+ warnOnSyncRender: false
18
153
  })
19
154
 
155
+ if (!headingAnchorOptions.enabled) {
156
+ return md;
157
+ }
158
+
20
159
  // Override heading renderer to add IDs and wrap with anchor links
21
160
  const defaultHeadingRender = md.renderer.rules.heading_open || function (tokens, idx, options, env, renderer) {
22
161
  return renderer.renderToken(tokens, idx, options)
@@ -27,17 +166,25 @@ export default function configureMarkdown(markdownit) {
27
166
  }
28
167
 
29
168
  md.renderer.rules.heading_open = function (tokens, idx, options, env, renderer) {
169
+ env = env || {};
30
170
  const token = tokens[idx]
31
171
  const nextToken = tokens[idx + 1]
32
- let slug = ''
172
+ let slug = headingAnchorOptions.fallback
33
173
 
34
174
  if (nextToken && nextToken.type === 'inline') {
35
175
  const headingText = nextToken.content
36
- slug = slugify(headingText)
37
- token.attrSet('id', slug)
176
+ const baseSlug = slugify(headingText, headingAnchorOptions)
177
+ const headingCounts = env.__rtglHeadingSlugCounts || (env.__rtglHeadingSlugCounts = new Map())
178
+ const nextCount = (headingCounts.get(baseSlug) || 0) + 1
179
+ headingCounts.set(baseSlug, nextCount)
180
+ slug = nextCount === 1 ? baseSlug : `${baseSlug}-${nextCount}`
38
181
  }
39
182
 
183
+ token.attrSet('id', slug)
40
184
  const headingHtml = defaultHeadingRender(tokens, idx, options, env, renderer)
185
+ if (!headingAnchorOptions.wrap) {
186
+ return headingHtml
187
+ }
41
188
  return `<a href="#${slug}" style="display: contents; text-decoration: none; color: inherit;">` + headingHtml
42
189
  }
43
190
 
@@ -46,4 +193,6 @@ export default function configureMarkdown(markdownit) {
46
193
  }
47
194
 
48
195
  return md
49
- }
196
+ }
197
+
198
+ export default createRtglMarkdown;
@@ -1,171 +1,7 @@
1
- import http from 'node:http';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
- import { existsSync } from 'node:fs';
5
- import { buildSite } from './cli/build.js';
6
- import { createScreenshotCapture } from './screenshot.js';
7
- import { loadSiteConfig } from './utils/loadSiteConfig.js';
8
-
9
- function getContentType(ext) {
10
- const types = {
11
- '.html': 'text/html',
12
- '.css': 'text/css',
13
- '.js': 'application/javascript',
14
- '.json': 'application/json',
15
- '.png': 'image/png',
16
- '.jpg': 'image/jpeg',
17
- '.gif': 'image/gif',
18
- '.svg': 'image/svg+xml',
19
- '.ico': 'image/x-icon'
20
- };
21
- return types[ext] || 'application/octet-stream';
22
- }
23
-
24
- function serveFile(filePath, res, skipWebSocket = false) {
25
- const ext = path.extname(filePath);
26
- const contentType = getContentType(ext);
27
-
28
- try {
29
- const content = fs.readFileSync(filePath);
30
- res.writeHead(200, { 'Content-Type': contentType });
31
- res.end(content);
32
- } catch (err) {
33
- console.error('Error serving file:', err);
34
- res.writeHead(500);
35
- res.end('Internal Server Error');
36
- }
37
- }
38
-
39
- function handleRequest(req, res, siteDir = '_site') {
40
- const urlParts = req.url.split('?');
41
- let urlPath = urlParts[0];
42
- const queryString = urlParts[1] || '';
43
-
44
- // Check if this is a screenshot request
45
- const isScreenshotRequest = queryString.includes('screenshot=true');
46
-
47
- // Default to index.html for root
48
- if (urlPath === '/') {
49
- urlPath = '/index.html';
50
- }
51
-
52
- // Handle trailing slash - remove it for processing
53
- const hasTrailingSlash = urlPath.endsWith('/') && urlPath !== '/';
54
- if (hasTrailingSlash) {
55
- urlPath = urlPath.slice(0, -1);
56
- }
57
-
58
- // Handle paths without extensions
59
- if (!path.extname(urlPath)) {
60
- // First try as .html file
61
- const htmlPath = path.join(siteDir, urlPath + '.html');
62
- if (existsSync(htmlPath)) {
63
- urlPath = urlPath + '.html';
64
- } else {
65
- // Try as directory with index.html
66
- const indexPath = path.join(siteDir, urlPath, 'index.html');
67
- if (existsSync(indexPath)) {
68
- urlPath = path.join(urlPath, 'index.html');
69
- }
70
- }
71
- }
72
-
73
- const filePath = path.join(siteDir, urlPath);
74
-
75
- // Check if file exists
76
- if (!existsSync(filePath)) {
77
- res.writeHead(404);
78
- res.end('404 Not Found');
79
- return;
80
- }
81
-
82
- // Check if it's a directory
83
- const stats = fs.statSync(filePath);
84
- if (stats.isDirectory()) {
85
- // Try to serve index.html from the directory
86
- const indexPath = path.join(filePath, 'index.html');
87
- if (existsSync(indexPath)) {
88
- return serveFile(indexPath, res, isScreenshotRequest);
89
- } else {
90
- res.writeHead(404);
91
- res.end('404 Not Found');
92
- return;
93
- }
94
- }
95
-
96
- // Serve the file
97
- serveFile(filePath, res, isScreenshotRequest);
98
- }
99
-
100
- function createTempServer(port = 3001, siteDir = '_site') {
101
- const httpServer = http.createServer((req, res) => {
102
- handleRequest(req, res, siteDir);
103
- });
104
-
105
- const start = () => {
106
- return new Promise((resolve, reject) => {
107
- httpServer.listen(port, '0.0.0.0', () => {
108
- console.log(`📡 Temp server running at http://localhost:${port}/`);
109
- resolve();
110
- });
111
-
112
- httpServer.on('error', reject);
113
- });
114
- };
115
-
116
- const close = () => {
117
- return new Promise((resolve) => {
118
- httpServer.close(() => {
119
- console.log('📡 Temp server closed');
120
- resolve();
121
- });
122
- });
123
- };
124
-
125
- return { start, close };
126
- }
127
-
128
- const screenshotCommand = async (options = {}) => {
129
- const {
130
- port = 3001,
131
- rootDir = '.'
132
- } = options;
133
-
134
- console.log('📸 Starting screenshot capture for all pages...');
135
-
136
- // Load config to get ignore patterns
137
- const config = await loadSiteConfig(rootDir, false);
138
- const ignorePatterns = config?.screenshots?.ignore || [];
139
-
140
- if (ignorePatterns.length > 0) {
141
- console.log('📋 Ignore patterns:', ignorePatterns);
142
- }
143
-
144
- // Build the site first (with screenshot mode enabled)
145
- console.log('Building site...');
146
- await buildSite({ rootDir, isScreenshotMode: true });
147
- console.log('Build complete');
148
-
149
- // Start temporary server
150
- const server = createTempServer(port);
151
- await server.start();
152
-
153
- // Initialize screenshot capture
154
- const screenshotCapture = await createScreenshotCapture(port);
155
-
156
- try {
157
- // Capture screenshots for all pages
158
- const pagesDir = path.join(rootDir, 'pages');
159
- await screenshotCapture.captureAllPages(pagesDir, { ignorePatterns });
160
-
161
- console.log('✅ All screenshots captured successfully!');
162
- } catch (error) {
163
- console.error('❌ Error capturing screenshots:', error);
164
- } finally {
165
- // Clean up
166
- await screenshotCapture.close();
167
- await server.close();
168
- }
1
+ const screenshotCommand = async () => {
2
+ throw new Error(
3
+ 'Screenshot CLI for @rettangoli/sites was removed. Use "rtgl vt generate" with "vt.url" and optional "vt.service.start".',
4
+ );
169
5
  };
170
6
 
171
- export default screenshotCommand;
7
+ export default screenshotCommand;