orga-build 0.5.2 → 0.5.4

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.
@@ -12,6 +12,7 @@ const outDir = path.join(__dirname, '.test-output')
12
12
  describe('orga-build', () => {
13
13
  before(async () => {
14
14
  await fs.mkdir(fixtureDir, { recursive: true })
15
+ await fs.mkdir(path.join(fixtureDir, 'docs'), { recursive: true })
15
16
  // Create minimal fixture
16
17
  await fs.writeFile(
17
18
  path.join(fixtureDir, 'index.org'),
@@ -20,8 +21,14 @@ describe('orga-build', () => {
20
21
  * Hello World
21
22
 
22
23
  This is a test page.
24
+
25
+ Here's [[file:./docs/index.org][index page]].
26
+
27
+ Here's [[file:more.org][another page]].
23
28
  `
24
29
  )
30
+ await fs.writeFile(path.join(fixtureDir, 'docs', 'index.org'), 'Docs index page.')
31
+ await fs.writeFile(path.join(fixtureDir, 'more.org'), 'Another page.')
25
32
  })
26
33
 
27
34
  after(async () => {
@@ -51,6 +58,8 @@ This is a test page.
51
58
  const html = await fs.readFile(indexPath, 'utf-8')
52
59
  assert.ok(html.includes('<title>Test Page</title>'), 'should have title')
53
60
  assert.ok(html.includes('Hello World'), 'should have heading content')
61
+ assert.ok(html.includes('href="/docs"'), 'should rewrite docs/index.org to /docs')
62
+ assert.ok(html.includes('href="/more"'), 'should rewrite more.org to /more')
54
63
  })
55
64
 
56
65
  test('generates assets directory', async () => {
package/lib/files.d.ts CHANGED
@@ -7,11 +7,16 @@ export function setup(dir: string, { outDir }?: {
7
7
  outDir?: string | undefined;
8
8
  }): {
9
9
  pages: () => Promise<Record<string, Page>>;
10
- page: (id: string) => Promise<Page>;
10
+ page: (slug: string) => Promise<Page>;
11
11
  components: () => Promise<string | null>;
12
12
  layouts: () => Promise<Record<string, string>>;
13
13
  contentEntries: () => Promise<ContentEntry[]>;
14
14
  };
15
+ /**
16
+ * Convert a content file path (relative to content root) to the canonical page slug.
17
+ * @param {string} contentFilePath
18
+ */
19
+ export function getSlugFromContentFilePath(contentFilePath: string): string;
15
20
  export type Page = {
16
21
  dataPath: string;
17
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"AA8EA;;;;GAIG;AACH,2BAJW,MAAM,eAEd;IAAyB,MAAM;CACjC;;eAiIY,MAAM;;;;EAKlB;;cAjNa,MAAM;;;;YACN,MAAM;;;QAMN,MAAM;UACN,MAAM;UACN,MAAM;cACN,MAAM;SACN,KAAK,GAAG,KAAK,GAAG,KAAK;UACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["files.js"],"names":[],"mappings":"AA8EA;;;;GAIG;AACH,2BAJW,MAAM,eAEd;IAAyB,MAAM;CACjC;;iBAiIY,MAAM;;;;EAKlB;AAsBD;;;GAGG;AACH,4DAFW,MAAM,UAoBhB;;cA7Pa,MAAM;;;;YACN,MAAM;;;QAMN,MAAM;UACN,MAAM;UACN,MAAM;cACN,MAAM;SACN,KAAK,GAAG,KAAK,GAAG,KAAK;UACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
package/lib/files.js CHANGED
@@ -106,8 +106,8 @@ export function setup(dir, { outDir } = {}) {
106
106
  /** @type {Record<string, Page>} */
107
107
  const pages = {}
108
108
  for (const file of files) {
109
- const pageId = getPagePublicPath(file)
110
- pages[pageId] = {
109
+ const pageSlug = getSlugFromContentFilePath(file)
110
+ pages[pageSlug] = {
111
111
  dataPath: path.join(dir, file)
112
112
  }
113
113
  }
@@ -131,8 +131,8 @@ export function setup(dir, { outDir } = {}) {
131
131
 
132
132
  return layoutFiles.reduce(
133
133
  (/** @type {Record<string, string>} */ result, file) => {
134
- const layoutPath = path.dirname(getPagePublicPath(file))
135
- result[layoutPath] = path.join(dir, file)
134
+ const layoutSlug = path.dirname(getSlugFromContentFilePath(file))
135
+ result[layoutSlug] = path.join(dir, file)
136
136
  return result
137
137
  },
138
138
  /** @type {Record<string, string>} */ {}
@@ -209,10 +209,10 @@ export function setup(dir, { outDir } = {}) {
209
209
 
210
210
  return files
211
211
 
212
- /** @param {string} id */
213
- async function page(id) {
212
+ /** @param {string} slug */
213
+ async function page(slug) {
214
214
  const all = await pages()
215
- return all[id]
215
+ return all[slug]
216
216
  }
217
217
  }
218
218
 
@@ -237,16 +237,18 @@ function cache(fn) {
237
237
  }
238
238
 
239
239
  /**
240
- * @param {string} file
240
+ * Convert a content file path (relative to content root) to the canonical page slug.
241
+ * @param {string} contentFilePath
241
242
  */
242
- function getPagePublicPath(file) {
243
- let pagePublicPath = file.replace(/\.(org|md|mdx|js|jsx|ts|tsx)$/, '')
244
- pagePublicPath = pagePublicPath.replace(/index$/, '')
243
+ export function getSlugFromContentFilePath(contentFilePath) {
244
+ const normalizedFilePath = contentFilePath.replace(/\\/g, '/')
245
+ let slug = normalizedFilePath.replace(/\.(org|md|mdx|js|jsx|ts|tsx)$/, '')
246
+ slug = slug.replace(/index$/, '')
245
247
  // remove trailing slash
246
- pagePublicPath = pagePublicPath.replace(/\/$/, '')
248
+ slug = slug.replace(/\/$/, '')
247
249
  // ensure starting slash
248
- pagePublicPath = pagePublicPath.replace(/^\//, '')
249
- pagePublicPath = `/${pagePublicPath}`
250
+ slug = slug.replace(/^\//, '')
251
+ slug = `/${slug}`
250
252
 
251
253
  // turn [id] into :id
252
254
  // so that react-router can recognize it as url params
@@ -255,5 +257,5 @@ function getPagePublicPath(file) {
255
257
  // (_, paramName) => `:${paramName}`
256
258
  // )
257
259
 
258
- return pagePublicPath
260
+ return slug
259
261
  }
package/lib/orga.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * @param {Object} options
3
3
  * @param {string|string[]} options.containerClass - CSS class name(s) to wrap the rendered content
4
+ * @param {string} options.root - Root directory for content files
4
5
  */
5
- export function setupOrga({ containerClass }: {
6
+ export function setupOrga({ containerClass, root }: {
6
7
  containerClass: string | string[];
8
+ root: string;
7
9
  }): import("@orgajs/rollup").Plugin;
8
10
  //# sourceMappingURL=orga.d.ts.map
package/lib/orga.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"orga.d.ts","sourceRoot":"","sources":["orga.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH,8CAFG;IAAiC,cAAc,EAAvC,MAAM,GAAC,MAAM,EAAE;CACzB,mCAaA"}
1
+ {"version":3,"file":"orga.d.ts","sourceRoot":"","sources":["orga.js"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,oDAHG;IAAiC,cAAc,EAAvC,MAAM,GAAC,MAAM,EAAE;IACC,IAAI,EAApB,MAAM;CAChB,mCAYA"}
package/lib/orga.js CHANGED
@@ -1,29 +1,62 @@
1
1
  /**
2
2
  * @import {Root as HastTree} from 'hast'
3
3
  */
4
+ import path from 'node:path'
4
5
  import _orga from '@orgajs/rollup'
5
6
  import { visitParents } from 'unist-util-visit-parents'
7
+ import { getSlugFromContentFilePath } from './files.js'
6
8
 
7
9
  /**
8
10
  * @param {Object} options
9
11
  * @param {string|string[]} options.containerClass - CSS class name(s) to wrap the rendered content
12
+ * @param {string} options.root - Root directory for content files
10
13
  */
11
- export function setupOrga({ containerClass }) {
14
+ export function setupOrga({ containerClass, root }) {
12
15
  return _orga({
13
- rehypePlugins: [[rehypeWrap, { className: containerClass }], image],
16
+ rehypePlugins: [
17
+ [rehypeWrap, { className: containerClass }],
18
+ [rewriteOrgFileLinks, { root }],
19
+ mediaAssets
20
+ ],
14
21
  reorgRehypeOptions: {
15
- linkHref: (link) => {
16
- if (link.path.protocol === 'file') {
17
- return link.path.value.replace(/\.org$/, '')
18
- }
19
- return link.path.value
20
- }
22
+ linkHref: (link) => link.path.value
21
23
  }
22
24
  })
23
25
  }
24
26
 
25
27
  // --- plugins ---
26
28
 
29
+ function mediaAssets() {
30
+ /**
31
+ * @param {any} tree
32
+ */
33
+ return function (tree) {
34
+ /** @type {Record<string, string>} */
35
+ const imports = {}
36
+ visitParents(tree, [{ tagName: 'img' }, { tagName: 'video' }], (node) => {
37
+ node.type = 'jsx'
38
+ const { src, ...rest } = node.properties
39
+ if (typeof src !== 'string') return
40
+ if (src.startsWith('http')) return
41
+ const tagName = node.tagName
42
+ const name = (imports[src] ??= `asset_${genId()}`)
43
+ const attrs = Object.entries(rest)
44
+ .filter(([, v]) => v !== undefined && v !== false)
45
+ .map(([k, v]) => (v === true ? k : `${k}='${v}'`))
46
+ .join(' ')
47
+ node.value = `<${tagName} src={${name}}${attrs ? ` ${attrs}` : ''}/>`
48
+ })
49
+
50
+ for (const [src, name] of Object.entries(imports)) {
51
+ tree.children.unshift({
52
+ type: 'jsx',
53
+ value: `import ${name} from '${src}'`,
54
+ children: []
55
+ })
56
+ }
57
+ }
58
+ }
59
+
27
60
  /**
28
61
  * @param {Object} options
29
62
  * @param {string[]} options.className
@@ -55,32 +88,54 @@ function rehypeWrap({ className = [] }) {
55
88
  }
56
89
  }
57
90
 
58
- function image() {
91
+ /**
92
+ * @param {Object} options
93
+ * @param {string} options.root
94
+ */
95
+ function rewriteOrgFileLinks({ root }) {
59
96
  /**
60
97
  * @param {any} tree
98
+ * @param {import('vfile').VFile} [file]
61
99
  */
62
- return function (tree) {
63
- /** @type {Record<string, string>} */
64
- const imports = {}
65
- visitParents(tree, { tagName: 'img' }, (node) => {
66
- node.type = 'jsx'
67
- const { src, target } = node.properties
68
- if (typeof src !== 'string') return
69
- if (src.startsWith('http')) {
70
- return
71
- }
72
- const name = (imports[src] ??= `asset_${genId()}`)
73
- node.value = `<img src={${name}} target='${target}'/>`
74
- })
100
+ return function (tree, file) {
101
+ const filePath = file?.path
102
+ if (!filePath) return
75
103
 
76
- for (const [src, name] of Object.entries(imports)) {
77
- tree.children.unshift({
78
- type: 'jsx',
79
- value: `import ${name} from '${src}'`,
80
- children: []
104
+ visitParents(tree, { tagName: 'a' }, (node) => {
105
+ const href = node?.properties?.href
106
+ if (typeof href !== 'string') return
107
+ if (!href.endsWith('.org')) return
108
+
109
+ const targetSlug = resolveOrgHrefToContentSlug({
110
+ root,
111
+ filePath,
112
+ href
81
113
  })
82
- }
114
+ if (!targetSlug) return
115
+ node.properties.href = targetSlug
116
+ })
117
+ }
118
+ }
119
+
120
+ /**
121
+ * @param {Object} options
122
+ * @param {string} options.root
123
+ * @param {string} options.filePath
124
+ * @param {string} options.href
125
+ * @returns {string|null}
126
+ */
127
+ function resolveOrgHrefToContentSlug({ root, filePath, href }) {
128
+ const decodedHrefPath = decodeURI(href)
129
+ const absoluteTargetPath = decodedHrefPath.startsWith('/')
130
+ ? path.resolve(root, `.${decodedHrefPath}`)
131
+ : path.resolve(path.dirname(filePath), decodedHrefPath)
132
+
133
+ const relativeTargetPath = path.relative(root, absoluteTargetPath)
134
+ if (relativeTargetPath.startsWith('..') || path.isAbsolute(relativeTargetPath)) {
135
+ return null
83
136
  }
137
+
138
+ return getSlugFromContentFilePath(relativeTargetPath)
84
139
  }
85
140
 
86
141
  function genId(length = 8) {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["plugin.js"],"names":[],"mappings":"AAoBA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,kEAHW,sBAAsB,GACpB,OAAO,MAAM,EAAE,YAAY,EAAE,CAIzC;AAED;;;;;;GAMG;AACH,uHAHW,sBAAsB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5I;IAAE,OAAO,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,KAAK,CAAA;KAAE,CAAA;CAAE,CAoBxF;AAiBD;;;;;;;;;;;;GAYG;AACH,gDAHW,MAAM,GACJ,OAAO,MAAM,EAAE,MAAM,CA8CjC;AA9HD;;GAEG;AACH;;;;EAIC;;;;;UAIa,MAAM;;;;aACN,MAAM,GAAG,SAAS;;;;qBAClB,MAAM,GAAC,MAAM,EAAE"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["plugin.js"],"names":[],"mappings":"AAoBA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,kEAHW,sBAAsB,GACpB,OAAO,MAAM,EAAE,YAAY,EAAE,CAQzC;AAED;;;;;;GAMG;AACH,uHAHW,sBAAsB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5I;IAAE,OAAO,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,KAAK,CAAA;KAAE,CAAA;CAAE,CAoBxF;AAiBD;;;;;;;;;;;;GAYG;AACH,gDAHW,MAAM,GACJ,OAAO,MAAM,EAAE,MAAM,CA8CjC;AAlID;;GAEG;AACH;;;;EAIC;;;;;UAIa,MAAM;;;;aACN,MAAM,GAAG,SAAS;;;;qBAClB,MAAM,GAAC,MAAM,EAAE"}
package/lib/plugin.js CHANGED
@@ -33,7 +33,11 @@ export const alias = {
33
33
  * @returns {import('vite').PluginOption[]}
34
34
  */
35
35
  export function orgaBuildPlugin({ root, outDir, containerClass = [] }) {
36
- return [setupOrga({ containerClass }), react(), pluginFactory({ dir: root, outDir })]
36
+ return [
37
+ setupOrga({ containerClass, root }),
38
+ react(),
39
+ pluginFactory({ dir: root, outDir })
40
+ ]
37
41
  }
38
42
 
39
43
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orga-build",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "A simple tool that builds org-mode files into a website",
5
5
  "type": "module",
6
6
  "engines": {