getfilepress 0.1.32 → 0.1.34

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": "getfilepress",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "FilePress — file-based Markdown blog engine. Link this package from a content-only site (config + posts), then run `filepress build`.",
@@ -106,6 +106,14 @@
106
106
  "node": ">=20"
107
107
  },
108
108
  "packageManager": "pnpm@10.30.1",
109
+ "pnpm": {
110
+ "overrides": {
111
+ "js-yaml": "3.15.2",
112
+ "nanoid": "3.3.18",
113
+ "postcss": "8.5.23",
114
+ "sharp": "0.35.4"
115
+ }
116
+ },
109
117
  "devDependencies": {
110
118
  "wrangler": "^4.120.1"
111
119
  }
@@ -33,7 +33,7 @@ function ensureGenieDirs(siteRoot: string) {
33
33
  }
34
34
 
35
35
  export function newVersionId(): string {
36
- const ts = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, 'Z');
36
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
37
37
  return `${ts}-${randomBytes(2).toString('hex')}`;
38
38
  }
39
39
 
@@ -7,7 +7,7 @@
7
7
  * keeps `@filepress/core` app-agnostic and importable from both client and
8
8
  * server code.
9
9
  */
10
- import { normalizePathMounts, type PathMount } from './paths-shared';
10
+ import { normalizePathMounts, stripTrailingSlashes, type PathMount } from './paths-shared';
11
11
  import { writingPostRedirects, type RedirectRule } from './redirects';
12
12
  export type { PathMount } from './paths-shared';
13
13
  export type { RedirectRule } from './redirects';
@@ -228,7 +228,7 @@ export function defineFilepressConfig(input: SiteConfigInput): SiteConfig {
228
228
  lede: (input.lede ?? '').trim() || null,
229
229
  logo,
230
230
  ogImage: (input.ogImage ?? '').trim() || logo,
231
- url: url.replace(/\/+$/, ''),
231
+ url: stripTrailingSlashes(url),
232
232
  author: (input.author ?? '').trim() || title,
233
233
  postsPerPage:
234
234
  Number.isFinite(input.postsPerPage) && (input.postsPerPage as number) > 0
@@ -279,7 +279,7 @@ function normalizeRedirects(rules: RedirectRule[] | undefined): RedirectRule[] {
279
279
 
280
280
  /** Join the site origin with a path, guarding against double slashes. */
281
281
  export function absoluteUrl(site: Pick<SiteConfig, 'url'>, path: string): string {
282
- const base = site.url.replace(/\/+$/, '');
282
+ const base = stripTrailingSlashes(site.url);
283
283
  const suffix = path.startsWith('/') ? path : `/${path}`;
284
284
  return `${base}${suffix}`;
285
285
  }
@@ -12,6 +12,17 @@ export interface PathMount {
12
12
  url: string;
13
13
  }
14
14
 
15
+ export function toPosixPath(path: string): string {
16
+ return path.split('\\').join('/');
17
+ }
18
+
19
+ /** Drop trailing `/` without a regular expression. */
20
+ export function stripTrailingSlashes(path: string): string {
21
+ let out = path;
22
+ while (out.length > 1 && out.endsWith('/')) out = out.slice(0, -1);
23
+ return out;
24
+ }
25
+
15
26
  /** Engine route prefixes a mount must not steal. */
16
27
  const ENGINE_URL_PREFIXES = new Set([
17
28
  ...RESERVED_PAGE_SLUGS,
@@ -38,8 +49,8 @@ export function normalizePathMounts(input: PathMount[] | undefined): PathMount[]
38
49
  if (!raw || typeof raw !== 'object') {
39
50
  throw new Error('filepress.config: each `paths` entry must be an object { url, dir }.');
40
51
  }
41
- const dir = String(raw.dir ?? '').trim().replace(/\\/g, '/').replace(/\/+$/, '');
42
- let url = String(raw.url ?? '').trim().replace(/\\/g, '/');
52
+ const dir = stripTrailingSlashes(toPosixPath(String(raw.dir ?? '').trim()));
53
+ let url = toPosixPath(String(raw.url ?? '').trim());
43
54
  if (!dir) {
44
55
  throw new Error('filepress.config: `paths[].dir` must be a non-empty site-relative path.');
45
56
  }
@@ -51,7 +62,7 @@ export function normalizePathMounts(input: PathMount[] | undefined): PathMount[]
51
62
  if (!url.startsWith('/')) {
52
63
  throw new Error(`filepress.config: \`paths[].url\` must start with / (got "${raw.url}").`);
53
64
  }
54
- url = url.replace(/\/+$/, '') || '/';
65
+ url = stripTrailingSlashes(url) || '/';
55
66
  if (url === '/') {
56
67
  throw new Error('filepress.config: `paths[].url` cannot be `/` (that is the site home).');
57
68
  }
@@ -2,6 +2,7 @@
2
2
  * Cloudflare Pages / Netlify `_redirects` lines.
3
3
  * Code owns the file shape; sites and import only supply from/to pairs.
4
4
  */
5
+ import { stripTrailingSlashes } from './paths-shared';
5
6
  export type RedirectStatus = 301 | 302 | 308;
6
7
 
7
8
  export type RedirectRule = {
@@ -18,11 +19,11 @@ export function normalizeRedirectPath(path: string): string {
18
19
  if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
19
20
  const url = new URL(trimmed);
20
21
  const out = url.pathname || '/';
21
- return out === '/' ? '/' : out.replace(/\/+$/, '') || '/';
22
+ return out === '/' ? '/' : stripTrailingSlashes(out) || '/';
22
23
  }
23
24
  const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
24
25
  if (withSlash.includes('*') || withSlash.includes(':splat')) return withSlash;
25
- return withSlash === '/' ? '/' : withSlash.replace(/\/+$/, '') || '/';
26
+ return withSlash === '/' ? '/' : stripTrailingSlashes(withSlash) || '/';
26
27
  }
27
28
 
28
29
  export function serializeRedirects(rules: RedirectRule[]): string {
@@ -33,7 +34,8 @@ export function serializeRedirects(rules: RedirectRule[]): string {
33
34
  export function parseRedirectsFile(text: string): RedirectRule[] {
34
35
  const rules: RedirectRule[] = [];
35
36
  for (const raw of text.split(/\r?\n/)) {
36
- const line = raw.replace(/#.*$/, '').trim();
37
+ const hash = raw.indexOf('#');
38
+ const line = (hash === -1 ? raw : raw.slice(0, hash)).trim();
37
39
  if (!line) continue;
38
40
  const parts = line.split(/\s+/);
39
41
  if (parts.length < 2) continue;
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
+ import { parseHTML } from 'linkedom';
3
4
  import type { DesignBrief, SiteIR } from './ir.ts';
4
5
  import type { InspirationSignals } from './inspire.ts';
5
6
  import { DEFAULT_BRIEF, parseBriefJson } from './theme.ts';
@@ -430,10 +431,11 @@ export async function generateDesignBrief(opts: {
430
431
 
431
432
  /** Short text summary of an inspiration homepage for the brief prompt. */
432
433
  export function summarizeHtmlForBrief(html: string, max = 1200): string {
433
- const text = html
434
- .replace(/<script[\s\S]*?<\/script>/gi, ' ')
435
- .replace(/<style[\s\S]*?<\/style>/gi, ' ')
436
- .replace(/<[^>]+>/g, ' ')
434
+ const { document } = parseHTML(html);
435
+ for (const node of document.querySelectorAll('script, style, noscript')) {
436
+ node.remove();
437
+ }
438
+ const text = (document.body?.textContent ?? document.documentElement?.textContent ?? '')
437
439
  .replace(/\s+/g, ' ')
438
440
  .trim();
439
441
  return text.slice(0, max);
@@ -2,6 +2,7 @@
2
2
  * WordPress REST overlay for filepress import.
3
3
  * Sitemap + HTML still own the crawl; this fills categories → tags and nav.
4
4
  */
5
+ import { parseHTML } from 'linkedom';
5
6
  import { fetchText, originOf } from './fetch.ts';
6
7
 
7
8
  export type WpTerm = {
@@ -40,18 +41,11 @@ const SKIP_CATEGORY = new Set(['uncategorized']);
40
41
  const SKIP_PAGE = new Set(['sample-page']);
41
42
 
42
43
  export function decodeWpText(raw: string): string {
43
- return raw
44
- .replace(/<[^>]+>/g, ' ')
45
- .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
46
- .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)))
47
- .replace(/&amp;/g, '&')
48
- .replace(/&lt;/g, '<')
49
- .replace(/&gt;/g, '>')
50
- .replace(/&quot;/g, '"')
51
- .replace(/&#039;|&apos;/g, "'")
52
- .replace(/&nbsp;/g, ' ')
53
- .replace(/\s+/g, ' ')
54
- .trim();
44
+ const { document } = parseHTML('<div id="fp-wp"></div>');
45
+ const el = document.getElementById('fp-wp');
46
+ if (!el) return raw.replace(/\s+/g, ' ').trim();
47
+ el.innerHTML = raw;
48
+ return (el.textContent ?? '').replace(/\s+/g, ' ').trim();
55
49
  }
56
50
 
57
51
  export function termsToTags(categories: WpTerm[], tags: WpTerm[], categoryIds: number[], tagIds: number[]): string[] {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { createServer } from 'node:http';
8
8
  import { existsSync, readFileSync, statSync } from 'node:fs';
9
- import { extname, join, resolve, sep } from 'node:path';
9
+ import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
 
12
12
  const MIME = {
@@ -32,16 +32,28 @@ const MIME = {
32
32
  const isMain =
33
33
  Boolean(process.argv[1]) && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
34
34
 
35
- function underRoot(root, abs) {
36
- return abs === root || abs.startsWith(root + sep);
35
+ function containedUnder(root, abs) {
36
+ const rel = relative(root, abs);
37
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
37
38
  }
38
39
 
39
40
  /** Map a request path to a file under `root`, or null. */
40
41
  export function resolveBuildFile(root, urlPath) {
41
- const pathname = decodeURIComponent((urlPath.split('?')[0] || '/'));
42
- const rel = pathname.replace(/^\/+/, '');
43
- const abs = resolve(root, rel);
44
- if (!underRoot(root, abs)) return null;
42
+ const base = resolve(root);
43
+ let pathname;
44
+ try {
45
+ pathname = decodeURIComponent((urlPath.split('?')[0] || '/'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ const parts = [];
50
+ for (const seg of pathname.split('/')) {
51
+ if (!seg || seg === '.') continue;
52
+ if (seg === '..') return null;
53
+ parts.push(seg);
54
+ }
55
+ const abs = parts.length ? join(base, ...parts) : base;
56
+ if (!containedUnder(base, abs)) return null;
45
57
  try {
46
58
  const st = statSync(abs);
47
59
  if (st.isFile()) return abs;