bunki 0.9.0 → 0.10.0
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/dist/cli/commands/validate.d.ts +5 -0
- package/dist/cli.js +180 -65
- package/dist/index.js +152 -64
- package/dist/parser.d.ts +6 -1
- package/dist/site-generator.d.ts +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/utils/date-utils.d.ts +18 -0
- package/dist/utils/markdown-utils.d.ts +11 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -32764,6 +32764,18 @@ function escape(html, encode) {
|
|
|
32764
32764
|
|
|
32765
32765
|
// src/utils/markdown-utils.ts
|
|
32766
32766
|
var import_sanitize_html = __toESM(require_sanitize_html(), 1);
|
|
32767
|
+
|
|
32768
|
+
// src/utils/date-utils.ts
|
|
32769
|
+
function toPacificTime(date) {
|
|
32770
|
+
return new Date(new Date(date).toLocaleString("en-US", {
|
|
32771
|
+
timeZone: "America/Los_Angeles"
|
|
32772
|
+
}));
|
|
32773
|
+
}
|
|
32774
|
+
function getPacificYear(date) {
|
|
32775
|
+
return toPacificTime(date).getFullYear();
|
|
32776
|
+
}
|
|
32777
|
+
|
|
32778
|
+
// src/utils/markdown-utils.ts
|
|
32767
32779
|
core_default.registerLanguage("javascript", javascript);
|
|
32768
32780
|
core_default.registerLanguage("typescript", typescript);
|
|
32769
32781
|
core_default.registerLanguage("markdown", markdown);
|
|
@@ -32890,20 +32902,36 @@ async function parseMarkdownFile(filePath) {
|
|
|
32890
32902
|
try {
|
|
32891
32903
|
const fileContent = await readFileAsText(filePath);
|
|
32892
32904
|
if (fileContent === null) {
|
|
32893
|
-
|
|
32894
|
-
|
|
32905
|
+
return {
|
|
32906
|
+
post: null,
|
|
32907
|
+
error: {
|
|
32908
|
+
file: filePath,
|
|
32909
|
+
type: "file_not_found",
|
|
32910
|
+
message: "File not found or couldn't be read"
|
|
32911
|
+
}
|
|
32912
|
+
};
|
|
32895
32913
|
}
|
|
32896
32914
|
const { data, content } = import_gray_matter.default(fileContent);
|
|
32897
32915
|
if (!data.title || !data.date) {
|
|
32898
|
-
|
|
32899
|
-
|
|
32916
|
+
const missingFields = [];
|
|
32917
|
+
if (!data.title)
|
|
32918
|
+
missingFields.push("title");
|
|
32919
|
+
if (!data.date)
|
|
32920
|
+
missingFields.push("date");
|
|
32921
|
+
return {
|
|
32922
|
+
post: null,
|
|
32923
|
+
error: {
|
|
32924
|
+
file: filePath,
|
|
32925
|
+
type: "missing_field",
|
|
32926
|
+
message: `Missing required fields: ${missingFields.join(", ")}`,
|
|
32927
|
+
suggestion: "Add required frontmatter fields (title and date)"
|
|
32928
|
+
}
|
|
32929
|
+
};
|
|
32900
32930
|
}
|
|
32901
32931
|
let slug = data.slug || getBaseFilename(filePath);
|
|
32902
32932
|
const sanitizedHtml = convertMarkdownToHtml(content);
|
|
32903
|
-
const pacificDate =
|
|
32904
|
-
|
|
32905
|
-
}));
|
|
32906
|
-
const postYear = pacificDate.getFullYear();
|
|
32933
|
+
const pacificDate = toPacificTime(data.date);
|
|
32934
|
+
const postYear = getPacificYear(data.date);
|
|
32907
32935
|
const post = {
|
|
32908
32936
|
title: data.title,
|
|
32909
32937
|
date: pacificDate.toISOString(),
|
|
@@ -32915,24 +32943,98 @@ async function parseMarkdownFile(filePath) {
|
|
|
32915
32943
|
excerpt: data.excerpt || extractExcerpt(content),
|
|
32916
32944
|
html: sanitizedHtml
|
|
32917
32945
|
};
|
|
32918
|
-
return post;
|
|
32946
|
+
return { post, error: null };
|
|
32919
32947
|
} catch (error) {
|
|
32920
|
-
|
|
32921
|
-
|
|
32948
|
+
const isYamlError = error?.name === "YAMLException" || error?.message?.includes("YAML") || error?.message?.includes("mapping pair");
|
|
32949
|
+
let suggestion;
|
|
32950
|
+
if (isYamlError) {
|
|
32951
|
+
if (error?.message?.includes("mapping pair") || error?.message?.includes("colon")) {
|
|
32952
|
+
suggestion = 'Quote titles/descriptions containing colons (e.g., title: "My Post: A Guide")';
|
|
32953
|
+
} else if (error?.message?.includes("multiline key")) {
|
|
32954
|
+
suggestion = "Remove nested quotes or use single quotes inside double quotes";
|
|
32955
|
+
}
|
|
32956
|
+
}
|
|
32957
|
+
return {
|
|
32958
|
+
post: null,
|
|
32959
|
+
error: {
|
|
32960
|
+
file: filePath,
|
|
32961
|
+
type: isYamlError ? "yaml" : "unknown",
|
|
32962
|
+
message: error?.message || String(error),
|
|
32963
|
+
suggestion
|
|
32964
|
+
}
|
|
32965
|
+
};
|
|
32922
32966
|
}
|
|
32923
32967
|
}
|
|
32924
32968
|
|
|
32925
32969
|
// src/parser.ts
|
|
32926
|
-
async function parseMarkdownDirectory(contentDir) {
|
|
32970
|
+
async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
32927
32971
|
try {
|
|
32928
32972
|
const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
|
|
32929
32973
|
console.log(`Found ${markdownFiles.length} markdown files`);
|
|
32930
|
-
const
|
|
32931
|
-
const
|
|
32932
|
-
|
|
32974
|
+
const resultsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath));
|
|
32975
|
+
const results = await Promise.all(resultsPromises);
|
|
32976
|
+
const posts = [];
|
|
32977
|
+
const errors = [];
|
|
32978
|
+
for (const result of results) {
|
|
32979
|
+
if (result.post) {
|
|
32980
|
+
posts.push(result.post);
|
|
32981
|
+
} else if (result.error) {
|
|
32982
|
+
errors.push(result.error);
|
|
32983
|
+
}
|
|
32984
|
+
}
|
|
32985
|
+
if (errors.length > 0) {
|
|
32986
|
+
console.error(`
|
|
32987
|
+
\u26A0\uFE0F Found ${errors.length} parsing error(s):
|
|
32988
|
+
`);
|
|
32989
|
+
const yamlErrors = errors.filter((e) => e.type === "yaml");
|
|
32990
|
+
const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
|
|
32991
|
+
const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field");
|
|
32992
|
+
if (yamlErrors.length > 0) {
|
|
32993
|
+
console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
|
|
32994
|
+
yamlErrors.slice(0, 5).forEach((e) => {
|
|
32995
|
+
console.error(` \u274C ${e.file}`);
|
|
32996
|
+
if (e.suggestion) {
|
|
32997
|
+
console.error(` \uD83D\uDCA1 ${e.suggestion}`);
|
|
32998
|
+
}
|
|
32999
|
+
});
|
|
33000
|
+
if (yamlErrors.length > 5) {
|
|
33001
|
+
console.error(` ... and ${yamlErrors.length - 5} more`);
|
|
33002
|
+
}
|
|
33003
|
+
console.error("");
|
|
33004
|
+
}
|
|
33005
|
+
if (missingFieldErrors.length > 0) {
|
|
33006
|
+
console.error(` Missing Required Fields (${missingFieldErrors.length}):`);
|
|
33007
|
+
missingFieldErrors.slice(0, 5).forEach((e) => {
|
|
33008
|
+
console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
|
|
33009
|
+
});
|
|
33010
|
+
if (missingFieldErrors.length > 5) {
|
|
33011
|
+
console.error(` ... and ${missingFieldErrors.length - 5} more`);
|
|
33012
|
+
}
|
|
33013
|
+
console.error("");
|
|
33014
|
+
}
|
|
33015
|
+
if (otherErrors.length > 0) {
|
|
33016
|
+
console.error(` Other Errors (${otherErrors.length}):`);
|
|
33017
|
+
otherErrors.slice(0, 3).forEach((e) => {
|
|
33018
|
+
console.error(` \u274C ${e.file}: ${e.message}`);
|
|
33019
|
+
});
|
|
33020
|
+
if (otherErrors.length > 3) {
|
|
33021
|
+
console.error(` ... and ${otherErrors.length - 3} more`);
|
|
33022
|
+
}
|
|
33023
|
+
console.error("");
|
|
33024
|
+
}
|
|
33025
|
+
console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
|
|
33026
|
+
console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
|
|
33027
|
+
`);
|
|
33028
|
+
if (strictMode) {
|
|
33029
|
+
throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
|
|
33030
|
+
}
|
|
33031
|
+
}
|
|
33032
|
+
const sortedPosts = posts.sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
|
|
33033
|
+
console.log(`Parsed ${sortedPosts.length} posts`);
|
|
33034
|
+
return sortedPosts;
|
|
32933
33035
|
} catch (error) {
|
|
32934
33036
|
console.error(`Error parsing markdown directory:`, error);
|
|
32935
|
-
|
|
33037
|
+
throw error;
|
|
32936
33038
|
}
|
|
32937
33039
|
}
|
|
32938
33040
|
|
|
@@ -33087,15 +33189,18 @@ class SiteGenerator {
|
|
|
33087
33189
|
options;
|
|
33088
33190
|
site;
|
|
33089
33191
|
formatRSSDate(date) {
|
|
33090
|
-
|
|
33091
|
-
timeZone: "America/Los_Angeles"
|
|
33092
|
-
}));
|
|
33093
|
-
return pacificDate.toUTCString();
|
|
33192
|
+
return toPacificTime(date).toUTCString();
|
|
33094
33193
|
}
|
|
33095
|
-
|
|
33096
|
-
|
|
33097
|
-
|
|
33098
|
-
|
|
33194
|
+
groupPostsByYear(posts) {
|
|
33195
|
+
const postsByYear = {};
|
|
33196
|
+
for (const post of posts) {
|
|
33197
|
+
const year = getPacificYear(post.date).toString();
|
|
33198
|
+
if (!postsByYear[year]) {
|
|
33199
|
+
postsByYear[year] = [];
|
|
33200
|
+
}
|
|
33201
|
+
postsByYear[year].push(post);
|
|
33202
|
+
}
|
|
33203
|
+
return postsByYear;
|
|
33099
33204
|
}
|
|
33100
33205
|
getSortedTags(limit) {
|
|
33101
33206
|
const sorted = Object.values(this.site.tags).sort((a, b2) => b2.count - a.count);
|
|
@@ -33121,17 +33226,15 @@ class SiteGenerator {
|
|
|
33121
33226
|
this.site = {
|
|
33122
33227
|
name: options2.config.domain,
|
|
33123
33228
|
posts: [],
|
|
33124
|
-
tags: {}
|
|
33229
|
+
tags: {},
|
|
33230
|
+
postsByYear: {}
|
|
33125
33231
|
};
|
|
33126
33232
|
const env = import_nunjucks.default.configure(this.options.templatesDir, {
|
|
33127
33233
|
autoescape: true,
|
|
33128
33234
|
watch: false
|
|
33129
33235
|
});
|
|
33130
33236
|
env.addFilter("date", function(date, format) {
|
|
33131
|
-
const
|
|
33132
|
-
timeZone: "America/Los_Angeles"
|
|
33133
|
-
});
|
|
33134
|
-
const d2 = new Date(pstDate);
|
|
33237
|
+
const d2 = toPacificTime(date);
|
|
33135
33238
|
const months = [
|
|
33136
33239
|
"January",
|
|
33137
33240
|
"February",
|
|
@@ -33178,8 +33281,8 @@ class SiteGenerator {
|
|
|
33178
33281
|
console.warn("Error loading tag descriptions:", error);
|
|
33179
33282
|
}
|
|
33180
33283
|
}
|
|
33181
|
-
const
|
|
33182
|
-
|
|
33284
|
+
const strictMode = this.options.config.strictMode ?? false;
|
|
33285
|
+
const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode);
|
|
33183
33286
|
const tags = {};
|
|
33184
33287
|
posts.forEach((post) => {
|
|
33185
33288
|
post.tagSlugs = {};
|
|
@@ -33205,34 +33308,28 @@ class SiteGenerator {
|
|
|
33205
33308
|
this.site = {
|
|
33206
33309
|
name: this.options.config.domain,
|
|
33207
33310
|
posts,
|
|
33208
|
-
tags
|
|
33311
|
+
tags,
|
|
33312
|
+
postsByYear: this.groupPostsByYear(posts)
|
|
33209
33313
|
};
|
|
33210
33314
|
}
|
|
33211
33315
|
async generate() {
|
|
33212
33316
|
console.log("Generating static site...");
|
|
33213
33317
|
await ensureDir(this.options.outputDir);
|
|
33214
33318
|
await this.generateStylesheet();
|
|
33215
|
-
await
|
|
33216
|
-
|
|
33217
|
-
|
|
33218
|
-
|
|
33219
|
-
|
|
33220
|
-
|
|
33221
|
-
|
|
33222
|
-
|
|
33319
|
+
await Promise.all([
|
|
33320
|
+
this.generateIndexPage(),
|
|
33321
|
+
this.generatePostPages(),
|
|
33322
|
+
this.generateTagPages(),
|
|
33323
|
+
this.generateYearArchives(),
|
|
33324
|
+
this.generateRSSFeed(),
|
|
33325
|
+
this.generateSitemap(),
|
|
33326
|
+
this.generateRobotsTxt(),
|
|
33327
|
+
this.copyStaticAssets()
|
|
33328
|
+
]);
|
|
33223
33329
|
console.log("Site generation complete!");
|
|
33224
33330
|
}
|
|
33225
33331
|
async generateYearArchives() {
|
|
33226
|
-
const postsByYear
|
|
33227
|
-
for (const post of this.site.posts) {
|
|
33228
|
-
const postDate = new Date(post.date);
|
|
33229
|
-
const year = postDate.getFullYear().toString();
|
|
33230
|
-
if (!postsByYear[year]) {
|
|
33231
|
-
postsByYear[year] = [];
|
|
33232
|
-
}
|
|
33233
|
-
postsByYear[year].push(post);
|
|
33234
|
-
}
|
|
33235
|
-
for (const [year, yearPosts] of Object.entries(postsByYear)) {
|
|
33332
|
+
for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
|
|
33236
33333
|
const yearDir = path5.join(this.options.outputDir, year);
|
|
33237
33334
|
await ensureDir(yearDir);
|
|
33238
33335
|
const pageSize = 10;
|
|
@@ -33453,7 +33550,7 @@ class SiteGenerator {
|
|
|
33453
33550
|
async generateRSSFeed() {
|
|
33454
33551
|
const posts = this.site.posts.slice(0, 15);
|
|
33455
33552
|
const config = this.options.config;
|
|
33456
|
-
const now =
|
|
33553
|
+
const now = toPacificTime(new Date);
|
|
33457
33554
|
const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
|
|
33458
33555
|
const lastBuildDate = this.formatRSSDate(latestPostDate);
|
|
33459
33556
|
const rssItems = posts.map((post) => {
|
|
@@ -33527,10 +33624,10 @@ ${rssItems}
|
|
|
33527
33624
|
await Bun.write(path5.join(this.options.outputDir, "feed.xml"), rssContent);
|
|
33528
33625
|
}
|
|
33529
33626
|
async generateSitemap() {
|
|
33530
|
-
const currentDate =
|
|
33627
|
+
const currentDate = toPacificTime(new Date).toISOString();
|
|
33531
33628
|
const pageSize = 10;
|
|
33532
33629
|
const config = this.options.config;
|
|
33533
|
-
const now =
|
|
33630
|
+
const now = toPacificTime(new Date).getTime();
|
|
33534
33631
|
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
33535
33632
|
const ONE_WEEK = 7 * ONE_DAY;
|
|
33536
33633
|
const ONE_MONTH = 30 * ONE_DAY;
|
|
@@ -33611,16 +33708,7 @@ ${rssItems}
|
|
|
33611
33708
|
}
|
|
33612
33709
|
}
|
|
33613
33710
|
}
|
|
33614
|
-
const postsByYear
|
|
33615
|
-
for (const post of this.site.posts) {
|
|
33616
|
-
const postDate = new Date(post.date);
|
|
33617
|
-
const year = postDate.getFullYear().toString();
|
|
33618
|
-
if (!postsByYear[year]) {
|
|
33619
|
-
postsByYear[year] = [];
|
|
33620
|
-
}
|
|
33621
|
-
postsByYear[year].push(post);
|
|
33622
|
-
}
|
|
33623
|
-
for (const [year, yearPosts] of Object.entries(postsByYear)) {
|
|
33711
|
+
for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
|
|
33624
33712
|
const currentYear = new Date().getFullYear();
|
|
33625
33713
|
const isCurrentYear = parseInt(year) === currentYear;
|
|
33626
33714
|
const yearPriority = isCurrentYear ? 0.7 : 0.5;
|
|
@@ -33654,7 +33742,7 @@ ${rssItems}
|
|
|
33654
33742
|
}
|
|
33655
33743
|
}
|
|
33656
33744
|
async generateSitemapIndex() {
|
|
33657
|
-
const currentDate =
|
|
33745
|
+
const currentDate = toPacificTime(new Date).toISOString();
|
|
33658
33746
|
const config = this.options.config;
|
|
33659
33747
|
let sitemapIndexContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
33660
33748
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
@@ -34690,6 +34778,32 @@ function registerServeCommand(program2) {
|
|
|
34690
34778
|
});
|
|
34691
34779
|
}
|
|
34692
34780
|
|
|
34781
|
+
// src/cli/commands/validate.ts
|
|
34782
|
+
function registerValidateCommand(program2) {
|
|
34783
|
+
program2.command("validate").description("Validate markdown files for parsing errors").option("-c, --config <path>", "Path to config file", "bunki.config.ts").action(async (options2) => {
|
|
34784
|
+
try {
|
|
34785
|
+
const config = await loadConfig(options2.config);
|
|
34786
|
+
console.log(`\uD83D\uDD0D Validating markdown files...
|
|
34787
|
+
`);
|
|
34788
|
+
const contentDir = "./content";
|
|
34789
|
+
try {
|
|
34790
|
+
await parseMarkdownDirectory(contentDir, true);
|
|
34791
|
+
console.log(`
|
|
34792
|
+
\u2705 All markdown files are valid!`);
|
|
34793
|
+
process.exit(0);
|
|
34794
|
+
} catch (error) {
|
|
34795
|
+
console.error(`
|
|
34796
|
+
\u274C Validation failed
|
|
34797
|
+
`);
|
|
34798
|
+
process.exit(1);
|
|
34799
|
+
}
|
|
34800
|
+
} catch (error) {
|
|
34801
|
+
console.error("Error during validation:", error.message);
|
|
34802
|
+
process.exit(1);
|
|
34803
|
+
}
|
|
34804
|
+
});
|
|
34805
|
+
}
|
|
34806
|
+
|
|
34693
34807
|
// src/cli.ts
|
|
34694
34808
|
var program2 = new Command;
|
|
34695
34809
|
registerInitCommand(program2);
|
|
@@ -34698,7 +34812,8 @@ registerGenerateCommand(program2);
|
|
|
34698
34812
|
registerServeCommand(program2);
|
|
34699
34813
|
registerCssCommand(program2);
|
|
34700
34814
|
registerImagesPushCommand(program2);
|
|
34701
|
-
program2
|
|
34815
|
+
registerValidateCommand(program2);
|
|
34816
|
+
program2.name("bunki").description("An opinionated static site generator built with Bun").version("0.10.0");
|
|
34702
34817
|
var currentFile = import.meta.url.replace("file://", "");
|
|
34703
34818
|
var mainFile = Bun.main;
|
|
34704
34819
|
if (currentFile === mainFile || currentFile.endsWith(mainFile)) {
|
package/dist/index.js
CHANGED
|
@@ -30382,6 +30382,18 @@ function escape(html, encode) {
|
|
|
30382
30382
|
|
|
30383
30383
|
// src/utils/markdown-utils.ts
|
|
30384
30384
|
var import_sanitize_html = __toESM(require_sanitize_html(), 1);
|
|
30385
|
+
|
|
30386
|
+
// src/utils/date-utils.ts
|
|
30387
|
+
function toPacificTime(date) {
|
|
30388
|
+
return new Date(new Date(date).toLocaleString("en-US", {
|
|
30389
|
+
timeZone: "America/Los_Angeles"
|
|
30390
|
+
}));
|
|
30391
|
+
}
|
|
30392
|
+
function getPacificYear(date) {
|
|
30393
|
+
return toPacificTime(date).getFullYear();
|
|
30394
|
+
}
|
|
30395
|
+
|
|
30396
|
+
// src/utils/markdown-utils.ts
|
|
30385
30397
|
core_default.registerLanguage("javascript", javascript);
|
|
30386
30398
|
core_default.registerLanguage("typescript", typescript);
|
|
30387
30399
|
core_default.registerLanguage("markdown", markdown);
|
|
@@ -30508,20 +30520,36 @@ async function parseMarkdownFile(filePath) {
|
|
|
30508
30520
|
try {
|
|
30509
30521
|
const fileContent = await readFileAsText(filePath);
|
|
30510
30522
|
if (fileContent === null) {
|
|
30511
|
-
|
|
30512
|
-
|
|
30523
|
+
return {
|
|
30524
|
+
post: null,
|
|
30525
|
+
error: {
|
|
30526
|
+
file: filePath,
|
|
30527
|
+
type: "file_not_found",
|
|
30528
|
+
message: "File not found or couldn't be read"
|
|
30529
|
+
}
|
|
30530
|
+
};
|
|
30513
30531
|
}
|
|
30514
30532
|
const { data, content } = import_gray_matter.default(fileContent);
|
|
30515
30533
|
if (!data.title || !data.date) {
|
|
30516
|
-
|
|
30517
|
-
|
|
30534
|
+
const missingFields = [];
|
|
30535
|
+
if (!data.title)
|
|
30536
|
+
missingFields.push("title");
|
|
30537
|
+
if (!data.date)
|
|
30538
|
+
missingFields.push("date");
|
|
30539
|
+
return {
|
|
30540
|
+
post: null,
|
|
30541
|
+
error: {
|
|
30542
|
+
file: filePath,
|
|
30543
|
+
type: "missing_field",
|
|
30544
|
+
message: `Missing required fields: ${missingFields.join(", ")}`,
|
|
30545
|
+
suggestion: "Add required frontmatter fields (title and date)"
|
|
30546
|
+
}
|
|
30547
|
+
};
|
|
30518
30548
|
}
|
|
30519
30549
|
let slug = data.slug || getBaseFilename(filePath);
|
|
30520
30550
|
const sanitizedHtml = convertMarkdownToHtml(content);
|
|
30521
|
-
const pacificDate =
|
|
30522
|
-
|
|
30523
|
-
}));
|
|
30524
|
-
const postYear = pacificDate.getFullYear();
|
|
30551
|
+
const pacificDate = toPacificTime(data.date);
|
|
30552
|
+
const postYear = getPacificYear(data.date);
|
|
30525
30553
|
const post = {
|
|
30526
30554
|
title: data.title,
|
|
30527
30555
|
date: pacificDate.toISOString(),
|
|
@@ -30533,24 +30561,98 @@ async function parseMarkdownFile(filePath) {
|
|
|
30533
30561
|
excerpt: data.excerpt || extractExcerpt(content),
|
|
30534
30562
|
html: sanitizedHtml
|
|
30535
30563
|
};
|
|
30536
|
-
return post;
|
|
30564
|
+
return { post, error: null };
|
|
30537
30565
|
} catch (error) {
|
|
30538
|
-
|
|
30539
|
-
|
|
30566
|
+
const isYamlError = error?.name === "YAMLException" || error?.message?.includes("YAML") || error?.message?.includes("mapping pair");
|
|
30567
|
+
let suggestion;
|
|
30568
|
+
if (isYamlError) {
|
|
30569
|
+
if (error?.message?.includes("mapping pair") || error?.message?.includes("colon")) {
|
|
30570
|
+
suggestion = 'Quote titles/descriptions containing colons (e.g., title: "My Post: A Guide")';
|
|
30571
|
+
} else if (error?.message?.includes("multiline key")) {
|
|
30572
|
+
suggestion = "Remove nested quotes or use single quotes inside double quotes";
|
|
30573
|
+
}
|
|
30574
|
+
}
|
|
30575
|
+
return {
|
|
30576
|
+
post: null,
|
|
30577
|
+
error: {
|
|
30578
|
+
file: filePath,
|
|
30579
|
+
type: isYamlError ? "yaml" : "unknown",
|
|
30580
|
+
message: error?.message || String(error),
|
|
30581
|
+
suggestion
|
|
30582
|
+
}
|
|
30583
|
+
};
|
|
30540
30584
|
}
|
|
30541
30585
|
}
|
|
30542
30586
|
|
|
30543
30587
|
// src/parser.ts
|
|
30544
|
-
async function parseMarkdownDirectory(contentDir) {
|
|
30588
|
+
async function parseMarkdownDirectory(contentDir, strictMode = false) {
|
|
30545
30589
|
try {
|
|
30546
30590
|
const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
|
|
30547
30591
|
console.log(`Found ${markdownFiles.length} markdown files`);
|
|
30548
|
-
const
|
|
30549
|
-
const
|
|
30550
|
-
|
|
30592
|
+
const resultsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath));
|
|
30593
|
+
const results = await Promise.all(resultsPromises);
|
|
30594
|
+
const posts = [];
|
|
30595
|
+
const errors = [];
|
|
30596
|
+
for (const result of results) {
|
|
30597
|
+
if (result.post) {
|
|
30598
|
+
posts.push(result.post);
|
|
30599
|
+
} else if (result.error) {
|
|
30600
|
+
errors.push(result.error);
|
|
30601
|
+
}
|
|
30602
|
+
}
|
|
30603
|
+
if (errors.length > 0) {
|
|
30604
|
+
console.error(`
|
|
30605
|
+
\u26A0\uFE0F Found ${errors.length} parsing error(s):
|
|
30606
|
+
`);
|
|
30607
|
+
const yamlErrors = errors.filter((e) => e.type === "yaml");
|
|
30608
|
+
const missingFieldErrors = errors.filter((e) => e.type === "missing_field");
|
|
30609
|
+
const otherErrors = errors.filter((e) => e.type !== "yaml" && e.type !== "missing_field");
|
|
30610
|
+
if (yamlErrors.length > 0) {
|
|
30611
|
+
console.error(` YAML Parsing Errors (${yamlErrors.length}):`);
|
|
30612
|
+
yamlErrors.slice(0, 5).forEach((e) => {
|
|
30613
|
+
console.error(` \u274C ${e.file}`);
|
|
30614
|
+
if (e.suggestion) {
|
|
30615
|
+
console.error(` \uD83D\uDCA1 ${e.suggestion}`);
|
|
30616
|
+
}
|
|
30617
|
+
});
|
|
30618
|
+
if (yamlErrors.length > 5) {
|
|
30619
|
+
console.error(` ... and ${yamlErrors.length - 5} more`);
|
|
30620
|
+
}
|
|
30621
|
+
console.error("");
|
|
30622
|
+
}
|
|
30623
|
+
if (missingFieldErrors.length > 0) {
|
|
30624
|
+
console.error(` Missing Required Fields (${missingFieldErrors.length}):`);
|
|
30625
|
+
missingFieldErrors.slice(0, 5).forEach((e) => {
|
|
30626
|
+
console.error(` \u26A0\uFE0F ${e.file}: ${e.message}`);
|
|
30627
|
+
});
|
|
30628
|
+
if (missingFieldErrors.length > 5) {
|
|
30629
|
+
console.error(` ... and ${missingFieldErrors.length - 5} more`);
|
|
30630
|
+
}
|
|
30631
|
+
console.error("");
|
|
30632
|
+
}
|
|
30633
|
+
if (otherErrors.length > 0) {
|
|
30634
|
+
console.error(` Other Errors (${otherErrors.length}):`);
|
|
30635
|
+
otherErrors.slice(0, 3).forEach((e) => {
|
|
30636
|
+
console.error(` \u274C ${e.file}: ${e.message}`);
|
|
30637
|
+
});
|
|
30638
|
+
if (otherErrors.length > 3) {
|
|
30639
|
+
console.error(` ... and ${otherErrors.length - 3} more`);
|
|
30640
|
+
}
|
|
30641
|
+
console.error("");
|
|
30642
|
+
}
|
|
30643
|
+
console.error(`\uD83D\uDCDD Tip: Fix YAML errors by quoting titles/descriptions with colons`);
|
|
30644
|
+
console.error(` Example: title: "My Post: A Guide" (quotes required for colons)
|
|
30645
|
+
`);
|
|
30646
|
+
if (strictMode) {
|
|
30647
|
+
throw new Error(`Build failed: ${errors.length} parsing error(s) found (strictMode enabled)`);
|
|
30648
|
+
}
|
|
30649
|
+
}
|
|
30650
|
+
const sortedPosts = posts.sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
|
|
30651
|
+
console.log(`Parsed ${sortedPosts.length} posts`);
|
|
30652
|
+
return sortedPosts;
|
|
30551
30653
|
} catch (error) {
|
|
30552
30654
|
console.error(`Error parsing markdown directory:`, error);
|
|
30553
|
-
|
|
30655
|
+
throw error;
|
|
30554
30656
|
}
|
|
30555
30657
|
}
|
|
30556
30658
|
// src/server.ts
|
|
@@ -31034,15 +31136,18 @@ class SiteGenerator {
|
|
|
31034
31136
|
options;
|
|
31035
31137
|
site;
|
|
31036
31138
|
formatRSSDate(date) {
|
|
31037
|
-
|
|
31038
|
-
timeZone: "America/Los_Angeles"
|
|
31039
|
-
}));
|
|
31040
|
-
return pacificDate.toUTCString();
|
|
31139
|
+
return toPacificTime(date).toUTCString();
|
|
31041
31140
|
}
|
|
31042
|
-
|
|
31043
|
-
|
|
31044
|
-
|
|
31045
|
-
|
|
31141
|
+
groupPostsByYear(posts) {
|
|
31142
|
+
const postsByYear = {};
|
|
31143
|
+
for (const post of posts) {
|
|
31144
|
+
const year = getPacificYear(post.date).toString();
|
|
31145
|
+
if (!postsByYear[year]) {
|
|
31146
|
+
postsByYear[year] = [];
|
|
31147
|
+
}
|
|
31148
|
+
postsByYear[year].push(post);
|
|
31149
|
+
}
|
|
31150
|
+
return postsByYear;
|
|
31046
31151
|
}
|
|
31047
31152
|
getSortedTags(limit) {
|
|
31048
31153
|
const sorted = Object.values(this.site.tags).sort((a, b2) => b2.count - a.count);
|
|
@@ -31068,17 +31173,15 @@ class SiteGenerator {
|
|
|
31068
31173
|
this.site = {
|
|
31069
31174
|
name: options2.config.domain,
|
|
31070
31175
|
posts: [],
|
|
31071
|
-
tags: {}
|
|
31176
|
+
tags: {},
|
|
31177
|
+
postsByYear: {}
|
|
31072
31178
|
};
|
|
31073
31179
|
const env = import_nunjucks.default.configure(this.options.templatesDir, {
|
|
31074
31180
|
autoescape: true,
|
|
31075
31181
|
watch: false
|
|
31076
31182
|
});
|
|
31077
31183
|
env.addFilter("date", function(date, format) {
|
|
31078
|
-
const
|
|
31079
|
-
timeZone: "America/Los_Angeles"
|
|
31080
|
-
});
|
|
31081
|
-
const d2 = new Date(pstDate);
|
|
31184
|
+
const d2 = toPacificTime(date);
|
|
31082
31185
|
const months = [
|
|
31083
31186
|
"January",
|
|
31084
31187
|
"February",
|
|
@@ -31125,8 +31228,8 @@ class SiteGenerator {
|
|
|
31125
31228
|
console.warn("Error loading tag descriptions:", error);
|
|
31126
31229
|
}
|
|
31127
31230
|
}
|
|
31128
|
-
const
|
|
31129
|
-
|
|
31231
|
+
const strictMode = this.options.config.strictMode ?? false;
|
|
31232
|
+
const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode);
|
|
31130
31233
|
const tags = {};
|
|
31131
31234
|
posts.forEach((post) => {
|
|
31132
31235
|
post.tagSlugs = {};
|
|
@@ -31152,34 +31255,28 @@ class SiteGenerator {
|
|
|
31152
31255
|
this.site = {
|
|
31153
31256
|
name: this.options.config.domain,
|
|
31154
31257
|
posts,
|
|
31155
|
-
tags
|
|
31258
|
+
tags,
|
|
31259
|
+
postsByYear: this.groupPostsByYear(posts)
|
|
31156
31260
|
};
|
|
31157
31261
|
}
|
|
31158
31262
|
async generate() {
|
|
31159
31263
|
console.log("Generating static site...");
|
|
31160
31264
|
await ensureDir(this.options.outputDir);
|
|
31161
31265
|
await this.generateStylesheet();
|
|
31162
|
-
await
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
|
|
31167
|
-
|
|
31168
|
-
|
|
31169
|
-
|
|
31266
|
+
await Promise.all([
|
|
31267
|
+
this.generateIndexPage(),
|
|
31268
|
+
this.generatePostPages(),
|
|
31269
|
+
this.generateTagPages(),
|
|
31270
|
+
this.generateYearArchives(),
|
|
31271
|
+
this.generateRSSFeed(),
|
|
31272
|
+
this.generateSitemap(),
|
|
31273
|
+
this.generateRobotsTxt(),
|
|
31274
|
+
this.copyStaticAssets()
|
|
31275
|
+
]);
|
|
31170
31276
|
console.log("Site generation complete!");
|
|
31171
31277
|
}
|
|
31172
31278
|
async generateYearArchives() {
|
|
31173
|
-
const postsByYear
|
|
31174
|
-
for (const post of this.site.posts) {
|
|
31175
|
-
const postDate = new Date(post.date);
|
|
31176
|
-
const year = postDate.getFullYear().toString();
|
|
31177
|
-
if (!postsByYear[year]) {
|
|
31178
|
-
postsByYear[year] = [];
|
|
31179
|
-
}
|
|
31180
|
-
postsByYear[year].push(post);
|
|
31181
|
-
}
|
|
31182
|
-
for (const [year, yearPosts] of Object.entries(postsByYear)) {
|
|
31279
|
+
for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
|
|
31183
31280
|
const yearDir = path5.join(this.options.outputDir, year);
|
|
31184
31281
|
await ensureDir(yearDir);
|
|
31185
31282
|
const pageSize = 10;
|
|
@@ -31400,7 +31497,7 @@ class SiteGenerator {
|
|
|
31400
31497
|
async generateRSSFeed() {
|
|
31401
31498
|
const posts = this.site.posts.slice(0, 15);
|
|
31402
31499
|
const config = this.options.config;
|
|
31403
|
-
const now =
|
|
31500
|
+
const now = toPacificTime(new Date);
|
|
31404
31501
|
const latestPostDate = posts.length > 0 ? posts[0].date : now.toISOString();
|
|
31405
31502
|
const lastBuildDate = this.formatRSSDate(latestPostDate);
|
|
31406
31503
|
const rssItems = posts.map((post) => {
|
|
@@ -31474,10 +31571,10 @@ ${rssItems}
|
|
|
31474
31571
|
await Bun.write(path5.join(this.options.outputDir, "feed.xml"), rssContent);
|
|
31475
31572
|
}
|
|
31476
31573
|
async generateSitemap() {
|
|
31477
|
-
const currentDate =
|
|
31574
|
+
const currentDate = toPacificTime(new Date).toISOString();
|
|
31478
31575
|
const pageSize = 10;
|
|
31479
31576
|
const config = this.options.config;
|
|
31480
|
-
const now =
|
|
31577
|
+
const now = toPacificTime(new Date).getTime();
|
|
31481
31578
|
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
31482
31579
|
const ONE_WEEK = 7 * ONE_DAY;
|
|
31483
31580
|
const ONE_MONTH = 30 * ONE_DAY;
|
|
@@ -31558,16 +31655,7 @@ ${rssItems}
|
|
|
31558
31655
|
}
|
|
31559
31656
|
}
|
|
31560
31657
|
}
|
|
31561
|
-
const postsByYear
|
|
31562
|
-
for (const post of this.site.posts) {
|
|
31563
|
-
const postDate = new Date(post.date);
|
|
31564
|
-
const year = postDate.getFullYear().toString();
|
|
31565
|
-
if (!postsByYear[year]) {
|
|
31566
|
-
postsByYear[year] = [];
|
|
31567
|
-
}
|
|
31568
|
-
postsByYear[year].push(post);
|
|
31569
|
-
}
|
|
31570
|
-
for (const [year, yearPosts] of Object.entries(postsByYear)) {
|
|
31658
|
+
for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
|
|
31571
31659
|
const currentYear = new Date().getFullYear();
|
|
31572
31660
|
const isCurrentYear = parseInt(year) === currentYear;
|
|
31573
31661
|
const yearPriority = isCurrentYear ? 0.7 : 0.5;
|
|
@@ -31601,7 +31689,7 @@ ${rssItems}
|
|
|
31601
31689
|
}
|
|
31602
31690
|
}
|
|
31603
31691
|
async generateSitemapIndex() {
|
|
31604
|
-
const currentDate =
|
|
31692
|
+
const currentDate = toPacificTime(new Date).toISOString();
|
|
31605
31693
|
const config = this.options.config;
|
|
31606
31694
|
let sitemapIndexContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
31607
31695
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { Post } from "./types";
|
|
2
|
-
|
|
2
|
+
import { type ParseError } from "./utils/markdown-utils";
|
|
3
|
+
export interface ParseResult {
|
|
4
|
+
posts: Post[];
|
|
5
|
+
errors: ParseError[];
|
|
6
|
+
}
|
|
7
|
+
export declare function parseMarkdownDirectory(contentDir: string, strictMode?: boolean): Promise<Post[]>;
|
package/dist/site-generator.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface SiteConfig {
|
|
|
68
68
|
webMaster?: string;
|
|
69
69
|
/** Copyright statement for RSS feed (e.g., "Copyright © 2025 Your Site Name") */
|
|
70
70
|
copyright?: string;
|
|
71
|
+
/** Strict mode: fail build on parsing errors (default: false) */
|
|
72
|
+
strictMode?: boolean;
|
|
71
73
|
/** Additional custom configuration options */
|
|
72
74
|
[key: string]: any;
|
|
73
75
|
}
|
|
@@ -132,6 +134,8 @@ export interface Site {
|
|
|
132
134
|
posts: Post[];
|
|
133
135
|
/** Map of tag names to tag data */
|
|
134
136
|
tags: Record<string, TagData>;
|
|
137
|
+
/** Posts grouped by year for efficient year archive generation */
|
|
138
|
+
postsByYear: Record<string, Post[]>;
|
|
135
139
|
}
|
|
136
140
|
/**
|
|
137
141
|
* Interface for uploaders (different services can implement this)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date utility functions for bunki
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Converts a date to Pacific Time (America/Los_Angeles timezone)
|
|
6
|
+
* This is used consistently across the codebase for date handling
|
|
7
|
+
*
|
|
8
|
+
* @param date - Date string or Date object to convert
|
|
9
|
+
* @returns Date object in Pacific timezone
|
|
10
|
+
*/
|
|
11
|
+
export declare function toPacificTime(date: string | Date): Date;
|
|
12
|
+
/**
|
|
13
|
+
* Gets the year from a date in Pacific timezone
|
|
14
|
+
*
|
|
15
|
+
* @param date - Date string or Date object
|
|
16
|
+
* @returns Year as number
|
|
17
|
+
*/
|
|
18
|
+
export declare function getPacificYear(date: string | Date): number;
|
|
@@ -2,4 +2,14 @@ import { Post } from "../types";
|
|
|
2
2
|
export declare function setNoFollowExceptions(exceptions: string[]): void;
|
|
3
3
|
export declare function extractExcerpt(content: string, maxLength?: number): string;
|
|
4
4
|
export declare function convertMarkdownToHtml(markdownContent: string): string;
|
|
5
|
-
export
|
|
5
|
+
export interface ParseError {
|
|
6
|
+
file: string;
|
|
7
|
+
type: "yaml" | "missing_field" | "file_not_found" | "unknown";
|
|
8
|
+
message: string;
|
|
9
|
+
suggestion?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ParseMarkdownResult {
|
|
12
|
+
post: Post | null;
|
|
13
|
+
error: ParseError | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseMarkdownFile(filePath: string): Promise<ParseMarkdownResult>;
|
package/package.json
CHANGED