@rettangoli/sites 0.1.0 → 0.2.0-rc1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/sites",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-rc1",
4
4
  "description": "Generate static sites using Markdown and YAML. Straightforward, zero-complexity. Complete toolkit for landing pages, blogs, documentation, admin dashboards, and more.git remote add origin git@github.com:yuusoft-org/sitic.git",
5
5
  "author": {
6
6
  "name": "Luciano Hanyon Wu",
@@ -17,22 +17,19 @@
17
17
  "templates"
18
18
  ],
19
19
  "dependencies": {
20
- "commander": "^13.1.0",
21
- "html-minifier-terser": "^7.2.0",
20
+ "jempl": "^0.2.0-rc1",
22
21
  "js-yaml": "^4.1.0",
23
- "liquidjs": "^10.21.0",
24
- "luxon": "^3.6.1",
25
22
  "markdown-it": "^14.1.0",
26
- "markdown-it-async": "^2.2.0",
27
- "shiki": "^3.3.0"
23
+ "yahtml": "^0.0.2-rc1"
28
24
  },
29
25
  "devDependencies": {
30
- "@types/bun": "^1.2.8"
26
+ "memfs": "^4.36.0",
27
+ "puty": "^0.0.4"
31
28
  },
32
- "type": "module",
33
- "bin": {
34
- "sitic": "src/cli.js"
29
+ "scripts": {
30
+ "test": "vitest run --reporter=verbose"
35
31
  },
32
+ "type": "module",
36
33
  "license": "MIT",
37
34
  "keywords": [
38
35
  "static",
package/src/cli/build.js CHANGED
@@ -1,166 +1,33 @@
1
- import { join, dirname } from "path";
2
- import { fileURLToPath } from "url";
3
- import { DateTime } from "luxon";
4
-
5
- // Create the equivalent of __dirname for ES modules
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
-
9
- import {
10
- safeYamlLoad,
11
- safeReadFile,
12
- deepMerge,
13
- createTemplateRenderer,
14
- createFolderIfNotExists,
15
- configureMarkdown,
16
- loadCollections,
17
- createFileFormatHandlers,
18
- loadItems,
19
- copyDirRecursive,
20
- } from "../common.js";
21
- import { rm } from "fs/promises";
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createSiteBuilder } from '../createSiteBuilder.js';
22
4
 
23
5
  /**
24
- * Copy pages to site with processing
25
- * @param {Object} options - Options for copying pages
26
- * @param {string} options.resourcesPath - Path to resources directory
27
- * @param {string} options.pagesPath - Path to pages directory
28
- * @param {string} options.outputPath - Path to output directory
6
+ * Build the static site
7
+ * @param {Object} options - Options for building the site
8
+ * @param {string} options.rootDir - Root directory of the site (defaults to cwd)
9
+ * @param {Object} options.mdRender - Optional markdown renderer
29
10
  */
30
- export const copyPagesToSite = async (options) => {
31
- const {
32
- resourcesPath = "./sitic",
33
- pagesPath = "./pages",
34
- outputPath = "./_site",
35
- } = options;
36
-
37
- const dataPath = join(resourcesPath, "data.yaml");
38
- const templatesPath = join(resourcesPath, "templates");
39
- const componentsPath = join(resourcesPath, "components");
40
- const recordsPath = join(resourcesPath, "records");
41
-
42
- // Load hello.yaml template data
43
- const inputYaml = await safeReadFile(dataPath);
44
-
45
- const templates = await loadItems({
46
- path: join(__dirname, "./templates"),
47
- name: "templates",
48
- isYaml: false,
49
- keepExtension: true,
50
- });
51
-
52
- if (templatesPath) {
53
- const customTemplates = await loadItems({
54
- path: templatesPath,
55
- name: "templates",
56
- isYaml: false,
57
- keepExtension: true,
58
- });
59
- Object.assign(templates, customTemplates);
11
+ export const buildSite = async (options = {}) => {
12
+ const { rootDir = process.cwd(), mdRender } = options;
13
+
14
+ // Try to load config file if it exists
15
+ let config = {};
16
+ if (!mdRender) {
17
+ try {
18
+ const configPath = path.join(rootDir, 'sites.config.js');
19
+ const configModule = await import(configPath);
20
+ config = configModule.default || {};
21
+ } catch (e) {
22
+ // Config file is optional, continue without it
23
+ }
60
24
  }
61
25
 
62
- // Load data
63
- const records = await loadItems({
64
- path: recordsPath,
65
- name: "records",
66
- isYaml: true,
26
+ const build = createSiteBuilder({
27
+ fs,
28
+ rootDir,
29
+ mdRender: mdRender || config.mdRender
67
30
  });
68
-
69
- const components = await loadItems({
70
- path: join(__dirname, "./components"),
71
- name: "components",
72
- isYaml: false,
73
- });
74
-
75
- if (componentsPath) {
76
- const customComponents = await loadItems({
77
- path: componentsPath,
78
- name: "components",
79
- isYaml: false,
80
- });
81
- Object.assign(components, customComponents);
82
- }
83
-
84
- const collections = await loadCollections(pagesPath);
85
-
86
- const liquidParse = createTemplateRenderer({
87
- templates,
88
- filters: {
89
- json: (obj) => JSON.stringify(obj),
90
- "json-escaped": (obj) => {
91
- if (!obj) {
92
- return "";
93
- }
94
- return encodeURIComponent(JSON.stringify(obj));
95
- },
96
- postDate: (dateObj) => {
97
- if (!dateObj || typeof dateObj !== 'string') {
98
- return ''; // Return empty string or some default value if dateObj is undefined or not a string
99
- }
100
- try {
101
- return DateTime.fromFormat(dateObj, "yyyy-MM-dd").toLocaleString(
102
- DateTime.DATE_MED
103
- );
104
- } catch (error) {
105
- console.error(`Error formatting date "${dateObj}":`, error.message);
106
- return dateObj; // Return the original date string if parsing fails
107
- }
108
- },
109
- },
110
- });
111
-
112
- let data;
113
- try {
114
- data = safeYamlLoad(liquidParse(inputYaml, { collections }));
115
- } catch (error) {
116
- console.error("Error creating template renderer:", error);
117
- throw error;
118
- }
119
-
120
- // Create global data object for templates
121
- const globalData = {
122
- data,
123
- collections,
124
- records,
125
- };
126
-
127
- const yamlComponentRenderer = (content) => {
128
- const renderedContent = liquidParse(content, globalData);
129
- const yamlContent = safeYamlLoad(renderedContent);
130
-
131
- return yamlContent
132
- .map(({ component, data }) => {
133
- const foundComponent = components[component];
134
- if (!foundComponent) {
135
- throw new Error(`Component not found for ${component}`);
136
- }
137
- return liquidParse(foundComponent, deepMerge(globalData, data));
138
- })
139
- .join("\n");
140
- };
141
-
142
- const md = configureMarkdown({
143
- yamlComponentRenderer,
144
- });
145
-
146
- const fileFormatHandlers = createFileFormatHandlers({
147
- basePath: pagesPath,
148
- templates,
149
- liquidParse,
150
- data: globalData.data,
151
- collections,
152
- md,
153
- });
154
-
155
- try {
156
- await rm(outputPath, { recursive: true, force: true });
157
- await createFolderIfNotExists(outputPath);
158
- await copyDirRecursive(pagesPath, outputPath, fileFormatHandlers);
159
- console.log(`Pages copied from ${pagesPath} to ${outputPath} successfully`);
160
- } catch (error) {
161
- console.error(
162
- `Error copying pages from ${pagesPath} to ${outputPath}:`,
163
- error
164
- );
165
- }
166
- };
31
+
32
+ build();
33
+ };
package/src/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { copyPagesToSite } from './build.js';
1
+ import { buildSite } from './build.js';
2
2
 
3
3
  export {
4
- copyPagesToSite,
4
+ buildSite,
5
5
  }
@@ -0,0 +1,261 @@
1
+ import { convertToHtml } from 'yahtml';
2
+ import { parseAndRender } from 'jempl';
3
+ import path from 'path';
4
+ import yaml from 'js-yaml';
5
+
6
+ import MarkdownIt from 'markdown-it';
7
+
8
+ export function createSiteBuilder({ fs, rootDir = '.', mdRender }) {
9
+ return function build() {
10
+ // Use provided mdRender or default to standard markdown-it
11
+ const md = mdRender || MarkdownIt();
12
+
13
+ // Read all partials and create a JSON object
14
+ const partialsDir = path.join(rootDir, 'partials');
15
+ const partials = {};
16
+
17
+ if (fs.existsSync(partialsDir)) {
18
+ const files = fs.readdirSync(partialsDir);
19
+ files.forEach(file => {
20
+ const filePath = path.join(partialsDir, file);
21
+ const fileContent = fs.readFileSync(filePath, 'utf8');
22
+ const nameWithoutExt = path.basename(file, path.extname(file));
23
+ // Convert partial content from YAML string to JSON
24
+ partials[nameWithoutExt] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA });
25
+ });
26
+ }
27
+
28
+ // Read all data files and create a JSON object
29
+ const dataDir = path.join(rootDir, 'data');
30
+ const globalData = {};
31
+
32
+ if (fs.existsSync(dataDir)) {
33
+ const files = fs.readdirSync(dataDir);
34
+ files.forEach(file => {
35
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
36
+ const filePath = path.join(dataDir, file);
37
+ const fileContent = fs.readFileSync(filePath, 'utf8');
38
+ const nameWithoutExt = path.basename(file, path.extname(file));
39
+ // Load YAML content and store under filename key
40
+ globalData[nameWithoutExt] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA });
41
+ }
42
+ });
43
+ }
44
+
45
+ // Read all templates and create a JSON object
46
+ const templatesDir = path.join(rootDir, 'templates');
47
+ const templates = {};
48
+
49
+ function readTemplatesRecursively(dir, basePath = '') {
50
+ if (!fs.existsSync(dir)) return;
51
+
52
+ const items = fs.readdirSync(dir, { withFileTypes: true });
53
+
54
+ items.forEach(item => {
55
+ const itemPath = path.join(dir, item.name);
56
+
57
+ if (item.isDirectory()) {
58
+ // Recursively read subdirectories
59
+ const newBasePath = basePath ? `${basePath}/${item.name}` : item.name;
60
+ readTemplatesRecursively(itemPath, newBasePath);
61
+ } else if (item.isFile() && item.name.endsWith('.yaml')) {
62
+ // Read and convert YAML file
63
+ const fileContent = fs.readFileSync(itemPath, 'utf8');
64
+ const nameWithoutExt = path.basename(item.name, '.yaml');
65
+ const templateKey = basePath ? `${basePath}/${nameWithoutExt}` : nameWithoutExt;
66
+ templates[templateKey] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA });
67
+ }
68
+ });
69
+ }
70
+
71
+ readTemplatesRecursively(templatesDir);
72
+
73
+ // Function to process a single page file
74
+ function processPage(pagePath, outputRelativePath, isMarkdown = false) {
75
+ console.log(`Processing ${pagePath}...`);
76
+
77
+ // Read page content
78
+ const pageFileContent = fs.readFileSync(pagePath, 'utf8');
79
+
80
+ // Extract frontmatter and content
81
+ const lines = pageFileContent.split('\n');
82
+ let frontmatterStart = -1;
83
+ let frontmatterEnd = -1;
84
+ let frontmatterCount = 0;
85
+
86
+ for (let i = 0; i < lines.length; i++) {
87
+ if (lines[i].trim() === '---') {
88
+ frontmatterCount++;
89
+ if (frontmatterCount === 1) {
90
+ frontmatterStart = i + 1;
91
+ } else if (frontmatterCount === 2) {
92
+ frontmatterEnd = i;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+
98
+ // Store frontmatter
99
+ let frontmatter = {};
100
+ if (frontmatterStart > 0 && frontmatterEnd > frontmatterStart) {
101
+ const frontmatterContent = lines.slice(frontmatterStart, frontmatterEnd).join('\n');
102
+ frontmatter = yaml.load(frontmatterContent, { schema: yaml.JSON_SCHEMA }) || {};
103
+ }
104
+
105
+ // Get content after frontmatter
106
+ const contentStart = frontmatterEnd + 1;
107
+ const rawContent = lines.slice(contentStart).join('\n').trim();
108
+
109
+ // Merge global data with frontmatter for the page context
110
+ const pageData = { ...globalData, ...frontmatter };
111
+
112
+ let processedPageContent;
113
+
114
+ if (isMarkdown) {
115
+ // Process markdown content with MarkdownIt
116
+ const htmlContent = md.render(rawContent);
117
+ // For markdown, store as raw HTML that will be inserted directly
118
+ processedPageContent = { __html: htmlContent };
119
+ } else {
120
+ // Convert YAML content to JSON
121
+ const pageContent = yaml.load(rawContent, { schema: yaml.JSON_SCHEMA });
122
+ // Process the page content to resolve any $partial references with page data
123
+ processedPageContent = parseAndRender(pageContent, pageData, { partials });
124
+ }
125
+
126
+ // Find the template specified in frontmatter
127
+ let templateToUse = null;
128
+ if (frontmatter.template) {
129
+ // Look up template by exact path
130
+ templateToUse = templates[frontmatter.template];
131
+ if (!templateToUse) {
132
+ throw new Error(`Template "${frontmatter.template}" not found in ${pagePath}. Available templates: ${Object.keys(templates).join(', ')}`);
133
+ }
134
+ }
135
+
136
+ // Use the template with jempl to render the processed page content
137
+ let htmlString;
138
+
139
+ if (isMarkdown) {
140
+ if (templateToUse) {
141
+ // For markdown with template, use a placeholder and replace after
142
+ const placeholder = '___MARKDOWN_CONTENT_PLACEHOLDER___';
143
+ const templateData = { ...pageData, content: placeholder };
144
+ const templateResult = parseAndRender(templateToUse, templateData, { partials });
145
+ htmlString = convertToHtml(templateResult);
146
+ // Replace the placeholder with actual HTML content
147
+ htmlString = htmlString.replace(placeholder, processedPageContent.__html);
148
+ } else {
149
+ // Markdown without template - use HTML directly
150
+ htmlString = processedPageContent.__html;
151
+ }
152
+ } else {
153
+ // YAML content
154
+ const templateData = { ...pageData, content: processedPageContent };
155
+ const result = templateToUse
156
+ ? parseAndRender(templateToUse, templateData, { partials })
157
+ : processedPageContent;
158
+ htmlString = convertToHtml(result);
159
+ }
160
+
161
+ // Create output directory if it doesn't exist
162
+ const outputPath = path.join(rootDir, '_site', outputRelativePath);
163
+ const outputDir = path.dirname(outputPath);
164
+ if (!fs.existsSync(outputDir)) {
165
+ fs.mkdirSync(outputDir, { recursive: true });
166
+ }
167
+
168
+ // Write HTML to output file
169
+ fs.writeFileSync(outputPath, htmlString);
170
+ console.log(` -> Written to ${outputPath}`);
171
+ }
172
+
173
+ // Process all YAML and Markdown files in pages directory recursively
174
+ function processAllPages(dir, basePath = '') {
175
+ const pagesDir = path.join(rootDir, 'pages');
176
+ const fullDir = path.join(pagesDir, basePath);
177
+
178
+ if (!fs.existsSync(fullDir)) return;
179
+
180
+ const items = fs.readdirSync(fullDir, { withFileTypes: true });
181
+
182
+ for (const item of items) {
183
+ const itemPath = path.join(fullDir, item.name);
184
+ const relativePath = basePath ? path.join(basePath, item.name) : item.name;
185
+
186
+ if (item.isDirectory()) {
187
+ // Recursively process subdirectories
188
+ processAllPages(dir, relativePath);
189
+ } else if (item.isFile()) {
190
+ if (item.name.endsWith('.yaml')) {
191
+ // Process YAML file
192
+ const outputFileName = item.name.replace('.yaml', '.html');
193
+ const outputRelativePath = basePath ? path.join(basePath, outputFileName) : outputFileName;
194
+ processPage(itemPath, outputRelativePath, false);
195
+ } else if (item.name.endsWith('.md')) {
196
+ // Process Markdown file
197
+ const outputFileName = item.name.replace('.md', '.html');
198
+ const outputRelativePath = basePath ? path.join(basePath, outputFileName) : outputFileName;
199
+ processPage(itemPath, outputRelativePath, true);
200
+ }
201
+ // Ignore other file types
202
+ }
203
+ }
204
+ }
205
+
206
+ // Function to copy static files recursively
207
+ function copyStaticFiles() {
208
+ const staticDir = path.join(rootDir, 'static');
209
+ const outputDir = path.join(rootDir, '_site');
210
+
211
+ if (!fs.existsSync(staticDir)) {
212
+ return;
213
+ }
214
+
215
+ // Ensure output directory exists
216
+ if (!fs.existsSync(outputDir)) {
217
+ fs.mkdirSync(outputDir, { recursive: true });
218
+ }
219
+
220
+ function copyRecursive(src, dest) {
221
+ const stats = fs.statSync(src);
222
+
223
+ if (stats.isDirectory()) {
224
+ // Create directory if it doesn't exist
225
+ if (!fs.existsSync(dest)) {
226
+ fs.mkdirSync(dest, { recursive: true });
227
+ }
228
+
229
+ // Copy all items in directory
230
+ const items = fs.readdirSync(src);
231
+ items.forEach(item => {
232
+ copyRecursive(path.join(src, item), path.join(dest, item));
233
+ });
234
+ } else if (stats.isFile()) {
235
+ // Copy file
236
+ fs.copyFileSync(src, dest);
237
+ console.log(` -> Copied ${src} to ${dest}`);
238
+ }
239
+ }
240
+
241
+ console.log('Copying static files...');
242
+ const items = fs.readdirSync(staticDir);
243
+ items.forEach(item => {
244
+ const srcPath = path.join(staticDir, item);
245
+ const destPath = path.join(outputDir, item);
246
+ copyRecursive(srcPath, destPath);
247
+ });
248
+ }
249
+
250
+ // Start build process
251
+ console.log('Starting build process...');
252
+
253
+ // Copy static files first (they can be overwritten by pages)
254
+ copyStaticFiles();
255
+
256
+ // Process all pages (can overwrite static files)
257
+ processAllPages('');
258
+
259
+ console.log('Build complete!');
260
+ };
261
+ }
package/src/index.js CHANGED
@@ -0,0 +1,3 @@
1
+ export { createSiteBuilder } from './createSiteBuilder.js';
2
+ export { createRtglMarkdown } from './rtglMarkdown.js';
3
+ export { default as rtglMarkdown } from './rtglMarkdown.js';
@@ -0,0 +1,126 @@
1
+ import MarkdownIt from 'markdown-it';
2
+
3
+ // Simple slug generation function
4
+ function generateSlug(text) {
5
+ return text
6
+ .toLowerCase()
7
+ .trim()
8
+ .replace(/[^\w\s-]/g, '')
9
+ .replace(/[\s_-]+/g, '-')
10
+ .replace(/^-+|-+$/g, '');
11
+ }
12
+
13
+ /**
14
+ * Custom Markdown renderer configuration for Rettangoli
15
+ * Adds rtgl-specific elements and styling
16
+ */
17
+ export const createRtglMarkdown = () => {
18
+ const md = MarkdownIt({
19
+ // Additional configuration can be added here
20
+ });
21
+
22
+ // Header configuration
23
+ md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
24
+ const token = tokens[idx];
25
+ const level = token.markup.length;
26
+ const inlineToken = tokens[idx + 1];
27
+ const headingText = inlineToken.content;
28
+ const id = generateSlug(headingText);
29
+
30
+ // Map heading levels to size values
31
+ const sizes = { 1: "h1", 2: "h2", 3: "h3", 4: "h4" };
32
+ const size = sizes[level] || "md";
33
+
34
+ return `<rtgl-text id="${id}" mt="lg" s="${size}" mb="md"> <a href="#${id}" style="display: contents;">`;
35
+ };
36
+
37
+ md.renderer.rules.heading_close = () => "</a></rtgl-text>\n";
38
+
39
+ // Paragraph configuration
40
+ md.renderer.rules.paragraph_open = (tokens, idx, options, env, self) => {
41
+ // Check if we're inside a list item
42
+ let isInListItem = false;
43
+ for (let i = idx - 1; i >= 0; i--) {
44
+ if (tokens[i].type === 'list_item_open') {
45
+ isInListItem = true;
46
+ break;
47
+ }
48
+ if (tokens[i].type === 'list_item_close') {
49
+ break;
50
+ }
51
+ }
52
+
53
+ // Don't wrap paragraphs in list items with rtgl-text
54
+ if (isInListItem) {
55
+ return '';
56
+ }
57
+ return `<rtgl-text s="bl" mb="lg">`;
58
+ };
59
+
60
+ md.renderer.rules.paragraph_close = (tokens, idx, options, env, self) => {
61
+ // Check if we're inside a list item
62
+ let isInListItem = false;
63
+ for (let i = idx - 1; i >= 0; i--) {
64
+ if (tokens[i].type === 'list_item_open') {
65
+ isInListItem = true;
66
+ break;
67
+ }
68
+ if (tokens[i].type === 'list_item_close') {
69
+ break;
70
+ }
71
+ }
72
+
73
+ // Don't wrap paragraphs in list items with rtgl-text
74
+ if (isInListItem) {
75
+ return '\n';
76
+ }
77
+ return "</rtgl-text>\n";
78
+ };
79
+
80
+ // Table configuration
81
+ md.renderer.rules.table_open = () => '<rtgl-view w="f">\n<table>';
82
+ md.renderer.rules.table_close = () => "</table>\n</rtgl-view>";
83
+
84
+ // Link configuration - add target="_blank" to all external links
85
+ md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
86
+ const token = tokens[idx];
87
+ const targetIndex = token.attrIndex("target");
88
+ const href =
89
+ (token.attrs && token.attrs.find((attr) => attr[0] === "href")?.[1]) ||
90
+ "";
91
+ const isExternal = href.startsWith("http") || href.startsWith("//");
92
+
93
+ // If this is an external link or already has target="_blank"
94
+ if (isExternal || targetIndex >= 0) {
95
+ if (targetIndex < 0) {
96
+ token.attrPush(["target", "_blank"]);
97
+ }
98
+ token.attrPush(["rel", "noreferrer"]);
99
+
100
+ // Find the next text token to use for the aria-label
101
+ let nextIdx = idx + 1;
102
+ let textContent = "";
103
+ while (nextIdx < tokens.length && tokens[nextIdx].type !== "link_close") {
104
+ if (tokens[nextIdx].type === "text") {
105
+ textContent += tokens[nextIdx].content;
106
+ }
107
+ nextIdx++;
108
+ }
109
+
110
+ // Add aria-label for external links
111
+ if (textContent.trim() && token.attrIndex("aria-label") < 0) {
112
+ token.attrPush([
113
+ "aria-label",
114
+ `${textContent.trim()} (opens in new tab)`,
115
+ ]);
116
+ }
117
+ }
118
+
119
+ return self.renderToken(tokens, idx, options);
120
+ };
121
+
122
+ return md;
123
+ };
124
+
125
+ // Export a default instance for convenience
126
+ export default createRtglMarkdown();