@levino/shipyard-base 0.9.0 → 0.9.1
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 +3 -1
- package/src/astro.ts +2 -2
- package/src/markdownProcessor.test.ts +2 -1
- package/src/remark/remarkAdmonitions.ts +9 -1
- package/src/remark/remarkBlockDirective.ts +5 -0
- package/src/schemas/config.ts +5 -0
- package/src/shiki/index.ts +6 -3
- package/src/tools/linkChecker.test.ts +6 -6
- package/src/tools/linkChecker.ts +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levino/shipyard-base",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Core layouts, components, and configuration for shipyard - a composable page builder for Astro",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -67,9 +67,11 @@
|
|
|
67
67
|
"@astrojs/markdown-remark": "^7.2.4",
|
|
68
68
|
"clsx": "^2.1.0",
|
|
69
69
|
"mdast-util-directive": "^3.0.0",
|
|
70
|
+
"mdast-util-to-hast": "^13.2.1",
|
|
70
71
|
"micromark-extension-directive": "^3.0.0",
|
|
71
72
|
"ramda": "^0.32.0",
|
|
72
73
|
"remark-directive": "^3.0.0",
|
|
74
|
+
"remark-parse": "^11.0.0",
|
|
73
75
|
"tailwind-merge": "^2.2.0",
|
|
74
76
|
"unified": "^11.0.0",
|
|
75
77
|
"unist-util-visit": "^5.0.0"
|
package/src/astro.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * as components from '
|
|
2
|
-
export * as layouts from '
|
|
1
|
+
export * as components from '../astro/components'
|
|
2
|
+
export * as layouts from '../astro/layouts'
|
|
@@ -16,6 +16,7 @@ interface ConfigUpdate {
|
|
|
16
16
|
const baseConfig: Config = {
|
|
17
17
|
title: 'Test',
|
|
18
18
|
brand: 'Test',
|
|
19
|
+
tagline: 'Test',
|
|
19
20
|
navigation: {},
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -83,7 +84,7 @@ describe('markdown processor', () => {
|
|
|
83
84
|
})
|
|
84
85
|
|
|
85
86
|
expect(logger.warn).toHaveBeenCalledOnce()
|
|
86
|
-
expect(logger.warn.mock.calls[0][0]).toContain('satteri')
|
|
87
|
+
expect(logger.warn.mock.calls[0]?.[0]).toContain('satteri')
|
|
87
88
|
expect(update?.markdown?.processor?.name).toBe(unified().name)
|
|
88
89
|
expect(update?.markdown?.processor?.options.remarkPlugins).toHaveLength(
|
|
89
90
|
SHIPYARD_PLUGIN_COUNT,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Root } from 'mdast'
|
|
2
2
|
import type { ContainerDirective } from 'mdast-util-directive'
|
|
3
|
+
// `mdast-util-to-hast` augments mdast's `Data` with `hName`/`hProperties`, which
|
|
4
|
+
// this plugin sets. Imported explicitly so the augmentation is present when a
|
|
5
|
+
// consumer compiles this file, not just via a transitive dev dependency.
|
|
6
|
+
import type {} from 'mdast-util-to-hast'
|
|
3
7
|
import { includes, pipe, toLower } from 'ramda'
|
|
4
8
|
import type { Plugin } from 'unified'
|
|
5
9
|
import { SKIP, visit } from 'unist-util-visit'
|
|
@@ -67,7 +71,11 @@ export const remarkAdmonitions: Plugin<[], Root> = () => {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
const type = toLower(directiveName) as AdmonitionType
|
|
70
|
-
|
|
74
|
+
// NOTE: container directives carry no `label` field. `:::note[Custom]`
|
|
75
|
+
// is parsed by mdast-util-directive into a first child paragraph with
|
|
76
|
+
// `data.directiveLabel === true`, so custom titles are not picked up
|
|
77
|
+
// here and the default title is used for every admonition.
|
|
78
|
+
const title = getDefaultTitle(type)
|
|
71
79
|
|
|
72
80
|
// Set up the data for hast transformation
|
|
73
81
|
if (!node.data) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Root } from 'mdast'
|
|
2
2
|
import { directiveFromMarkdown } from 'mdast-util-directive'
|
|
3
3
|
import { directive } from 'micromark-extension-directive'
|
|
4
|
+
// `remark-parse` augments unified's `Data` with `micromarkExtensions` and
|
|
5
|
+
// `fromMarkdownExtensions`. shipyard ships raw TypeScript source, so a consumer
|
|
6
|
+
// compiles this file in their own program — importing the augmentation here
|
|
7
|
+
// keeps it available there instead of relying on a transitive dev dependency.
|
|
8
|
+
import type {} from 'remark-parse'
|
|
4
9
|
import type { Plugin, Processor } from 'unified'
|
|
5
10
|
|
|
6
11
|
/**
|
package/src/schemas/config.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// `astroHTML.JSX` is a global namespace declared by Astro. It is only in scope
|
|
2
|
+
// for files that pull in Astro's JSX types, which `.ts` files in this package
|
|
3
|
+
// do not do implicitly. The reference is erased at compile time.
|
|
4
|
+
/// <reference types="astro/astro-jsx" />
|
|
5
|
+
|
|
1
6
|
export interface NavigationEntry {
|
|
2
7
|
label?: string
|
|
3
8
|
href?: string
|
package/src/shiki/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Supports Docusaurus-style features like line numbers, line highlighting, and code block titles.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ElementContent } from 'hast'
|
|
6
7
|
import {
|
|
7
8
|
complement,
|
|
8
9
|
filter,
|
|
@@ -29,7 +30,9 @@ const parseLineRange = (rangeStr: string): number[] => {
|
|
|
29
30
|
const parseSegment = (segment: string): number[] => {
|
|
30
31
|
const trimmed = trim(segment)
|
|
31
32
|
if (test(/-/, trimmed)) {
|
|
32
|
-
const [
|
|
33
|
+
const [startStr, endStr] = split('-', trimmed)
|
|
34
|
+
const start = Number(startStr)
|
|
35
|
+
const end = Number(endStr)
|
|
33
36
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
|
34
37
|
}
|
|
35
38
|
return [Number(trimmed)]
|
|
@@ -79,7 +82,7 @@ const extractCodeMeta = (meta: string | undefined): CodeMeta => {
|
|
|
79
82
|
const startLineNumber = startLineMatch[1] ? Number(startLineMatch[1]) : 1
|
|
80
83
|
|
|
81
84
|
return {
|
|
82
|
-
title,
|
|
85
|
+
...(isNil(title) ? {} : { title }),
|
|
83
86
|
highlightLines,
|
|
84
87
|
showLineNumbers,
|
|
85
88
|
startLineNumber,
|
|
@@ -198,7 +201,7 @@ export const transformerMagicComments = (): ShikiTransformer => {
|
|
|
198
201
|
const lines = node.children.filter((child) => child.type === 'element')
|
|
199
202
|
lines.forEach((lineNode, index) => {
|
|
200
203
|
// Extract text content from line
|
|
201
|
-
const getText = (n:
|
|
204
|
+
const getText = (n: ElementContent): string => {
|
|
202
205
|
if (n.type === 'text') return n.value
|
|
203
206
|
if (n.type === 'element' && n.children) {
|
|
204
207
|
return n.children.map(getText).join('')
|
|
@@ -44,8 +44,8 @@ describe('checkLinks', () => {
|
|
|
44
44
|
const result = checkLinks(testDir)
|
|
45
45
|
|
|
46
46
|
expect(result.brokenCount).toBe(1)
|
|
47
|
-
expect(result.brokenLinks[0]
|
|
48
|
-
expect(result.brokenLinks[0]
|
|
47
|
+
expect(result.brokenLinks[0]?.href).toBe('/non-existent')
|
|
48
|
+
expect(result.brokenLinks[0]?.sourceFile).toBe('index.html')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
test('ignores external links', () => {
|
|
@@ -163,7 +163,7 @@ describe('checkLinks', () => {
|
|
|
163
163
|
|
|
164
164
|
const result = checkLinks(testDir)
|
|
165
165
|
|
|
166
|
-
expect(result.brokenLinks[0]
|
|
166
|
+
expect(result.brokenLinks[0]?.line).toBe(2)
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
169
|
|
|
@@ -221,7 +221,7 @@ describe('reportBrokenLinks', () => {
|
|
|
221
221
|
reportBrokenLinks(result, 'log', logger as any)
|
|
222
222
|
|
|
223
223
|
expect(logger.info).toHaveBeenCalled()
|
|
224
|
-
const message = logger.info.mock.calls[0][0]
|
|
224
|
+
const message = logger.info.mock.calls[0]?.[0]
|
|
225
225
|
expect(message).toContain('Found 1 broken link')
|
|
226
226
|
expect(message).toContain('/broken')
|
|
227
227
|
expect(message).toContain('index.html:1')
|
|
@@ -238,7 +238,7 @@ describe('reportBrokenLinks', () => {
|
|
|
238
238
|
reportBrokenLinks(result, 'warn', logger as any)
|
|
239
239
|
|
|
240
240
|
expect(logger.warn).toHaveBeenCalled()
|
|
241
|
-
const message = logger.warn.mock.calls[0][0]
|
|
241
|
+
const message = logger.warn.mock.calls[0]?.[0]
|
|
242
242
|
expect(message).toContain('Found 1 broken link')
|
|
243
243
|
})
|
|
244
244
|
|
|
@@ -269,7 +269,7 @@ describe('reportBrokenLinks', () => {
|
|
|
269
269
|
|
|
270
270
|
reportBrokenLinks(result, 'warn', logger as any)
|
|
271
271
|
|
|
272
|
-
const message = logger.warn.mock.calls[0][0]
|
|
272
|
+
const message = logger.warn.mock.calls[0]?.[0]
|
|
273
273
|
expect(message).toContain('Found 3 broken links')
|
|
274
274
|
})
|
|
275
275
|
})
|
package/src/tools/linkChecker.ts
CHANGED
|
@@ -52,16 +52,15 @@ function extractInternalLinks(html: string): { href: string; line: number }[] {
|
|
|
52
52
|
|
|
53
53
|
const hrefRegex = /href=["']([^"']+)["']/g
|
|
54
54
|
|
|
55
|
-
for (
|
|
56
|
-
const line = lines[i]
|
|
57
|
-
|
|
55
|
+
for (const [index, line] of lines.entries()) {
|
|
58
56
|
for (const match of line.matchAll(hrefRegex)) {
|
|
59
57
|
const href = match[1]
|
|
60
58
|
|
|
61
59
|
// Only include internal links (start with /)
|
|
62
60
|
// This automatically excludes external URLs, anchors, and special protocols
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// (the capture group is mandatory, so `href` is always a string here)
|
|
62
|
+
if (href?.startsWith('/')) {
|
|
63
|
+
links.push({ href, line: index + 1 })
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
}
|
|
@@ -75,7 +74,10 @@ function extractInternalLinks(html: string): { href: string; line: number }[] {
|
|
|
75
74
|
*/
|
|
76
75
|
function normalizePath(href: string): string[] {
|
|
77
76
|
// Remove query string and hash, and decode URL-encoded characters
|
|
78
|
-
|
|
77
|
+
// (`split` always yields at least one element, so the defaults never apply)
|
|
78
|
+
const [beforeQuery = ''] = href.split('?')
|
|
79
|
+
const [pathname = ''] = beforeQuery.split('#')
|
|
80
|
+
const path = decodeURIComponent(pathname)
|
|
79
81
|
|
|
80
82
|
// Generate possible file paths to check
|
|
81
83
|
const paths: string[] = []
|