livemark 0.16.0 → 0.17.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.
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from "vite-plus/test"
2
- import { matchSection, partitionBySection } from "./section.ts"
2
+ import {
3
+ customSectionActive,
4
+ matchSection,
5
+ partitionBySection,
6
+ } from "./section.ts"
3
7
 
4
8
  describe("matchSection", () => {
5
9
  const sections = [
@@ -69,3 +73,41 @@ describe("partitionBySection", () => {
69
73
  ])
70
74
  })
71
75
  })
76
+
77
+ describe("customSectionActive", () => {
78
+ it("should not match an absolute http url", () => {
79
+ expect(customSectionActive("https://x.com/y", "/y/")).toBe(false)
80
+ expect(customSectionActive("http://x.com/y", "/y/")).toBe(false)
81
+ })
82
+
83
+ it("should not match a protocol-relative url", () => {
84
+ expect(customSectionActive("//cdn.x.com/y", "/y/")).toBe(false)
85
+ })
86
+
87
+ it("should not match a hash url", () => {
88
+ expect(customSectionActive("#section", "/section/")).toBe(false)
89
+ })
90
+
91
+ it("should match the exact path", () => {
92
+ expect(customSectionActive("/about", "/about/")).toBe(true)
93
+ expect(customSectionActive("/about/", "/about/")).toBe(true)
94
+ })
95
+
96
+ it("should match a descendant path", () => {
97
+ expect(customSectionActive("/about", "/about/team/")).toBe(true)
98
+ })
99
+
100
+ it("should not match a sibling path with shared prefix", () => {
101
+ expect(customSectionActive("/about", "/aboutus/")).toBe(false)
102
+ })
103
+
104
+ it("should match exact root only", () => {
105
+ expect(customSectionActive("/", "/")).toBe(true)
106
+ expect(customSectionActive("/", "/about/")).toBe(false)
107
+ })
108
+
109
+ it("should ignore query and hash on the url", () => {
110
+ expect(customSectionActive("/about?x=1", "/about/")).toBe(true)
111
+ expect(customSectionActive("/about#top", "/about/team/")).toBe(true)
112
+ })
113
+ })
@@ -15,22 +15,33 @@ export type ViteOverride =
15
15
  | ViteUserConfig
16
16
  | ((env: ConfigEnv) => ViteUserConfig | Promise<ViteUserConfig>)
17
17
 
18
+ /** Sub-path mount. String form is used as-is; function form is called
19
+ * with Vite's `command` (`"serve"` in dev, `"build"` in production) so
20
+ * the user can branch (typical case: serve at `/` in dev but at
21
+ * `/repo/` in production for GitHub Pages). The build pipeline
22
+ * resolves it to a string before any runtime sees it. */
23
+ export type Base = string | ((command: "build" | "serve") => string | undefined)
24
+
18
25
  /**
19
26
  * User-provided configuration for Livemark.
27
+ *
28
+ * `vite` and `base` are lifted from `unknown` to their public types so
29
+ * `defineConfig` shows the right shape in editors. Use this type for
30
+ * the input to `defineConfig` — it keeps defaulted fields optional
31
+ * (because zod input ≠ output for fields with `.default()`).
20
32
  */
21
- export type UserConfig = Omit<z.infer<typeof UserConfig>, "vite"> & {
33
+ export type UserConfig = Omit<z.input<typeof UserConfig>, "vite" | "base"> & {
22
34
  vite?: ViteOverride
35
+ base?: Base
23
36
  }
24
37
  export const UserConfig = z.object({
25
38
  site: z.string().optional(),
26
- base: z
27
- .string()
28
- .optional()
29
- .transform(v => {
30
- if (!v) return undefined
31
- const trimmed = v.replace(/^\/|\/$/g, "")
32
- return trimmed ? `/${trimmed}` : undefined
33
- }),
39
+ // Stored as `unknown` in the schema (zod can't type a function and
40
+ // string union meaningfully); the public TypeScript surface above
41
+ // lifts it back to `Base` for editor support. The build pipeline
42
+ // resolves it via `resolveBase()` before any runtime consumer sees
43
+ // it, so the runtime `WebsiteConfig.base` is strictly `string`.
44
+ base: z.unknown().optional(),
34
45
  favicon: z.string().optional(),
35
46
  logo: z.string().optional(),
36
47
  title: z.string().default("Livemark"),
@@ -48,17 +59,20 @@ export const UserConfig = z.object({
48
59
  vite: z.unknown().optional(),
49
60
  })
50
61
 
51
- /** Website configuration injected at build time via Vite define */
62
+ /** Website configuration injected at build time via Vite define.
63
+ * `base` is strictly a string here — the build pipeline has already
64
+ * resolved any function form via `resolveBase()`. */
52
65
  export type WebsiteConfig = z.infer<typeof WebsiteConfig>
53
66
  export const WebsiteConfig = UserConfig.pick({
54
67
  title: true,
55
68
  description: true,
56
69
  site: true,
57
- base: true,
58
70
  favicon: true,
59
71
  logo: true,
60
72
  links: true,
61
73
  sections: true,
74
+ }).extend({
75
+ base: z.string().optional(),
62
76
  })
63
77
 
64
78
  /**
@@ -73,9 +87,13 @@ export const SystemConfig = z.object({
73
87
  /**
74
88
  * Fully resolved Livemark configuration.
75
89
  *
76
- * `vite` stays `unknown` here because that's what the runtime schema
90
+ * `vite` stays `unknown` because that's what the runtime schema
77
91
  * produces — consumer-facing typing happens via {@link UserConfig} so
78
- * `defineConfig` shows the right shape in editors.
92
+ * `defineConfig` shows the right shape in editors. `base` is lifted to
93
+ * `string | undefined` because `loadConfig()` resolves any function
94
+ * form via `resolveBase()` before returning.
79
95
  */
80
- export type Config = z.infer<typeof Config>
96
+ export type Config = Omit<z.infer<typeof Config>, "base"> & {
97
+ base?: string
98
+ }
81
99
  export const Config = UserConfig.merge(SystemConfig)
@@ -29,10 +29,12 @@ export const ChangelogSection = BaseSection.extend({
29
29
  version: z.boolean().optional(),
30
30
  })
31
31
 
32
- /** External section: renders as a link to an arbitrary URL. */
33
- export type ExternalSection = z.infer<typeof ExternalSection>
34
- export const ExternalSection = BaseSection.extend({
35
- type: z.literal("external"),
32
+ /** Custom section: renders as a link to an arbitrary URL — absolute
33
+ * (`https://…`) or internal (`/changelog`). Internal URLs participate
34
+ * in active-state highlighting like routed sections. */
35
+ export type CustomSection = z.infer<typeof CustomSection>
36
+ export const CustomSection = BaseSection.extend({
37
+ type: z.literal("custom"),
36
38
  url: z.string(),
37
39
  })
38
40
 
@@ -54,6 +56,6 @@ export const Section = z.preprocess(
54
56
  ArticleSection,
55
57
  BlogSection,
56
58
  ChangelogSection,
57
- ExternalSection,
59
+ CustomSection,
58
60
  ]),
59
61
  )