@kirchdev/duxt 0.3.4-dev-main.41-23ded41 → 0.3.5-dev-main.43-3310f4b

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.
Files changed (219) hide show
  1. package/README.md +24 -9
  2. package/app/app.vue +17 -11
  3. package/app/assets/css/duxt.css +47 -6
  4. package/app/components/DuxtAnnouncements.vue +117 -0
  5. package/app/components/DuxtBrunoEntries.vue +62 -0
  6. package/app/components/DuxtCodeBlock.vue +18 -34
  7. package/app/components/DuxtCodeToolbar.vue +14 -0
  8. package/app/components/DuxtCopyButton.vue +33 -0
  9. package/app/components/DuxtCopyPage.vue +32 -30
  10. package/app/components/DuxtFooter.vue +39 -8
  11. package/app/components/DuxtHeader.vue +107 -48
  12. package/app/components/DuxtLandingProse.vue +1 -1
  13. package/app/components/DuxtLinkLabel.vue +36 -0
  14. package/app/components/DuxtLiveWindow.vue +8 -8
  15. package/app/components/DuxtLocale.vue +10 -1
  16. package/app/components/DuxtNavigation.vue +2 -2
  17. package/app/components/DuxtOpenApiClient.vue +285 -90
  18. package/app/components/DuxtOpenApiMedia.vue +30 -18
  19. package/app/components/DuxtOpenApiSchema.vue +10 -7
  20. package/app/components/DuxtPageFeedback.vue +7 -0
  21. package/app/components/DuxtPageInfo.vue +15 -12
  22. package/app/components/DuxtPageNav.vue +41 -27
  23. package/app/components/DuxtPreviewSkeleton.vue +4 -4
  24. package/app/components/DuxtProgress.vue +2 -14
  25. package/app/components/DuxtSearch.vue +230 -90
  26. package/app/components/DuxtSearchInput.vue +70 -0
  27. package/app/components/DuxtSections.vue +11 -1
  28. package/app/components/DuxtSegmented.vue +64 -0
  29. package/app/components/DuxtShortcuts.vue +16 -11
  30. package/app/components/DuxtShortcutsTrigger.vue +49 -0
  31. package/app/components/DuxtSkipLink.vue +1 -1
  32. package/app/components/DuxtToc.vue +16 -10
  33. package/app/components/DuxtTranslationBanner.vue +1 -1
  34. package/app/components/DuxtVersion.vue +53 -12
  35. package/app/components/DuxtVersionBanner.vue +5 -5
  36. package/app/components/content/AccordionItem.vue +2 -2
  37. package/app/components/content/BrunoOverview.vue +260 -0
  38. package/app/components/content/BrunoRequest.vue +141 -0
  39. package/app/components/content/BrunoRequests.vue +104 -0
  40. package/app/components/content/Callout.vue +5 -5
  41. package/app/components/content/ChangelogReleases.vue +161 -40
  42. package/app/components/content/CodeGroup.vue +41 -37
  43. package/app/components/content/Field.vue +1 -1
  44. package/app/components/content/FileTree.vue +1 -1
  45. package/app/components/content/OpenApiOperation.vue +4 -4
  46. package/app/components/content/OpenApiOperations.vue +1 -1
  47. package/app/components/content/OpenApiOverview.vue +1 -1
  48. package/app/components/content/PackageManagers.vue +42 -58
  49. package/app/components/content/PageCards.vue +7 -4
  50. package/app/components/content/Preview.vue +15 -39
  51. package/app/components/content/ProseImg.vue +596 -49
  52. package/app/components/content/Since.vue +1 -1
  53. package/app/components/content/Steps.vue +6 -4
  54. package/app/components/content/TabGroup.vue +13 -12
  55. package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
  56. package/app/components/ui/command/CommandShortcut.vue +1 -1
  57. package/app/components/ui/dialog/DialogContent.vue +2 -2
  58. package/app/components/ui/dialog/DialogHeader.vue +1 -1
  59. package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
  60. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
  61. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  62. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
  63. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
  64. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
  66. package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
  67. package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
  68. package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
  69. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  70. package/app/components/ui/select/SelectItem.vue +2 -2
  71. package/app/components/ui/sheet/SheetContent.vue +29 -3
  72. package/app/components/ui/sidebar/Sidebar.vue +28 -4
  73. package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
  74. package/app/components/ui/sidebar/SidebarInset.vue +1 -1
  75. package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
  76. package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
  77. package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
  78. package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
  79. package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
  80. package/app/components/ui/sidebar/index.ts +10 -2
  81. package/app/components/ui/sidebar/utils.ts +0 -1
  82. package/app/components/ui/sonner/Sonner.vue +18 -0
  83. package/app/components/ui/tabs/TabsList.vue +13 -3
  84. package/app/components/ui/tabs/TabsTrigger.vue +24 -3
  85. package/app/components/ui/tooltip/TooltipContent.vue +2 -2
  86. package/app/composables/useActiveHeading.ts +3 -25
  87. package/app/composables/useDuxtAnalytics.ts +50 -0
  88. package/app/composables/useDuxtAnimatedHeight.ts +8 -38
  89. package/app/composables/useDuxtAnnouncements.ts +153 -0
  90. package/app/composables/useDuxtCopy.ts +51 -0
  91. package/app/composables/useDuxtDirection.ts +23 -0
  92. package/app/composables/useDuxtNavigation.ts +28 -8
  93. package/app/composables/useDuxtPath.ts +3 -5
  94. package/app/composables/useDuxtSearch.ts +87 -35
  95. package/app/composables/useDuxtSection.ts +15 -1
  96. package/app/composables/useDuxtSectionRow.ts +6 -5
  97. package/app/composables/useDuxtShortcuts.ts +319 -20
  98. package/app/composables/useDuxtVersion.ts +57 -6
  99. package/app/composables/useDuxtViewportMeasure.ts +37 -0
  100. package/app/composables/useFuzzySearch.ts +9 -4
  101. package/app/composables/useRecentPages.ts +14 -17
  102. package/app/error.vue +112 -74
  103. package/app/layouts/changelog.vue +9 -1
  104. package/app/layouts/default.vue +9 -0
  105. package/app/layouts/docs.vue +9 -1
  106. package/app/layouts/reference.vue +9 -1
  107. package/app/middleware/duxt-section-layout.global.ts +1 -3
  108. package/app/pages/[...slug].vue +146 -35
  109. package/app/pages/index.vue +15 -23
  110. package/app/types/duxt.d.ts +312 -4
  111. package/app/utils/analytics.ts +91 -0
  112. package/app/utils/announcements.ts +205 -0
  113. package/app/utils/changelog.ts +0 -28
  114. package/app/utils/code-highlight.ts +3 -1
  115. package/app/utils/contributor-avatar.ts +27 -0
  116. package/app/utils/direction.ts +43 -0
  117. package/app/utils/duxt-config.ts +43 -0
  118. package/app/utils/duxt-text.ts +10 -0
  119. package/app/utils/format-date.ts +28 -0
  120. package/app/utils/generated-sections.ts +68 -12
  121. package/app/utils/generated-toc.ts +14 -7
  122. package/app/utils/locale-path.ts +43 -0
  123. package/app/utils/navigation-tree.ts +51 -1
  124. package/app/utils/nearest-page.ts +8 -0
  125. package/app/utils/openapi.ts +16 -11
  126. package/app/utils/page-controls.ts +215 -0
  127. package/app/utils/pill.ts +61 -0
  128. package/app/utils/prose-image.ts +128 -0
  129. package/app/utils/search-context.ts +110 -0
  130. package/app/utils/search-display.ts +248 -0
  131. package/app/utils/section-scope.ts +65 -4
  132. package/app/utils/stored-json.ts +31 -0
  133. package/app/utils/version-choices.ts +6 -13
  134. package/app/utils/version-paths.ts +21 -0
  135. package/bin/duxt-cache-key.mjs +61 -0
  136. package/bin/duxt-og-cache.mjs +58 -0
  137. package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
  138. package/bruno-model.ts +169 -0
  139. package/bruno-parse.ts +656 -0
  140. package/bruno-zip.ts +195 -0
  141. package/cli.ts +240 -0
  142. package/duxt-app-config.ts +18 -1
  143. package/frontmatter.ts +49 -0
  144. package/git-contributors.ts +322 -0
  145. package/i18n/locales/de/duxt/announcement.json +7 -0
  146. package/i18n/locales/de/duxt/bruno.json +25 -0
  147. package/i18n/locales/de/duxt/changelog.json +1 -1
  148. package/i18n/locales/de/duxt/code.json +2 -1
  149. package/i18n/locales/de/duxt/page.json +9 -0
  150. package/i18n/locales/de/duxt/shortcuts.json +1 -0
  151. package/i18n/locales/de/duxt/theme.json +2 -1
  152. package/i18n/locales/en/duxt/announcement.json +7 -0
  153. package/i18n/locales/en/duxt/bruno.json +25 -0
  154. package/i18n/locales/en/duxt/changelog.json +1 -1
  155. package/i18n/locales/en/duxt/code.json +2 -1
  156. package/i18n/locales/en/duxt/page.json +9 -0
  157. package/i18n/locales/en/duxt/shortcuts.json +1 -0
  158. package/i18n/locales/en/duxt/theme.json +2 -1
  159. package/i18n/locales/es/duxt/announcement.json +7 -0
  160. package/i18n/locales/es/duxt/bruno.json +25 -0
  161. package/i18n/locales/es/duxt/changelog.json +1 -1
  162. package/i18n/locales/es/duxt/code.json +2 -1
  163. package/i18n/locales/es/duxt/page.json +9 -0
  164. package/i18n/locales/es/duxt/shortcuts.json +1 -0
  165. package/i18n/locales/es/duxt/theme.json +2 -1
  166. package/i18n/locales/fr/duxt/announcement.json +7 -0
  167. package/i18n/locales/fr/duxt/bruno.json +25 -0
  168. package/i18n/locales/fr/duxt/changelog.json +1 -1
  169. package/i18n/locales/fr/duxt/code.json +2 -1
  170. package/i18n/locales/fr/duxt/page.json +9 -0
  171. package/i18n/locales/fr/duxt/shortcuts.json +1 -0
  172. package/i18n/locales/fr/duxt/theme.json +2 -1
  173. package/i18n/locales/pt/duxt/announcement.json +7 -0
  174. package/i18n/locales/pt/duxt/bruno.json +25 -0
  175. package/i18n/locales/pt/duxt/changelog.json +1 -1
  176. package/i18n/locales/pt/duxt/code.json +2 -1
  177. package/i18n/locales/pt/duxt/page.json +9 -0
  178. package/i18n/locales/pt/duxt/shortcuts.json +1 -0
  179. package/i18n/locales/pt/duxt/theme.json +2 -1
  180. package/modules/bruno.ts +118 -0
  181. package/modules/config.ts +26 -0
  182. package/modules/devtools.ts +2 -3
  183. package/modules/git-meta.ts +16 -47
  184. package/modules/search-records.ts +132 -0
  185. package/nuxt.config.ts +81 -7
  186. package/og-image-cache.ts +742 -0
  187. package/package.json +41 -9
  188. package/public/devtools/checks.html +2 -2
  189. package/public/devtools/config.html +59 -4
  190. package/public/devtools/pages.html +8 -1
  191. package/public/devtools/paths.html +1 -1
  192. package/public/devtools/sources.html +2 -2
  193. package/public/devtools/versions.html +3 -0
  194. package/report.ts +28 -19
  195. package/search-records.ts +380 -0
  196. package/section-input.ts +142 -0
  197. package/section-markdown.ts +67 -0
  198. package/section-reports.ts +10 -4
  199. package/sections-bruno.ts +571 -0
  200. package/sections-changelog.ts +76 -43
  201. package/sections-openapi.ts +12 -83
  202. package/sections-resolve.ts +174 -8
  203. package/sections.ts +33 -10
  204. package/server/devtools/shell.ts +1 -1
  205. package/server/mcp/tools/list-pages.ts +77 -36
  206. package/server/mcp/tools/list-versions.ts +84 -0
  207. package/server/mcp/tools/read-page.ts +80 -26
  208. package/server/mcp/tools/search-docs.ts +87 -53
  209. package/server/middleware/raw-markdown.ts +1 -1
  210. package/server/routes/llms-full.txt.get.ts +6 -8
  211. package/server/routes/llms.txt.get.ts +1 -1
  212. package/server/utils/duxt-server-text.ts +0 -13
  213. package/server/utils/llms-pages.ts +24 -9
  214. package/server/utils/mcp-docs.ts +577 -0
  215. package/sources-cache.ts +376 -0
  216. package/sources-git.ts +186 -31
  217. package/sources-resolve.ts +59 -1
  218. package/sources.ts +122 -13
  219. package/tfplugindocs.ts +158 -0
@@ -0,0 +1,322 @@
1
+ import { execFileSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Who wrote something, read out of git.
5
+ *
6
+ * TWO SURFACES ASK THIS, and they ask it about different things. A page asks
7
+ * who touched the file it was parsed from — `modules/git-meta.ts`, indexed by
8
+ * path. A release asks who wrote the commits it carries — the release history,
9
+ * indexed by tag. What they share is the part that decides WHO A PERSON IS, and
10
+ * that is the whole reason this file exists rather than two copies of it: an
11
+ * identity computed twice is one reader shown as two people on one page and as
12
+ * one on the next.
13
+ *
14
+ * Node-only, and a root module rather than something under `modules/`, for the
15
+ * reason `repository-root.ts` is one: `sections-changelog.ts` reaches it out of
16
+ * the Content config, and a Nuxt module drags a type graph that config has no
17
+ * business resolving.
18
+ *
19
+ * NO EMAIL EVER LEAVES THIS FILE. It is the identity every answer below is
20
+ * keyed by, and it is the one field a commit carries that a published page has
21
+ * no business republishing: an address in a commit object is between a
22
+ * repository and whoever clones it, where the same address on a prerendered,
23
+ * crawled, indexed page is a harvesting target the person never agreed to.
24
+ * `DuxtContributor` is therefore a name, a count and — where git itself
25
+ * carries one — a GitHub handle, which is already public by construction.
26
+ */
27
+ export interface DuxtContributor {
28
+ name: string;
29
+ commits: number;
30
+ username?: string;
31
+ }
32
+
33
+ /** The fields either caller supplies about one commit. */
34
+ export interface DuxtCommitAuthor {
35
+ name: string;
36
+ email: string;
37
+ }
38
+
39
+ /**
40
+ * One entry per person, most commits first.
41
+ *
42
+ * Identity is the email, not the name — the same person commits as "Titus
43
+ * Kirch" and as "titus" and would otherwise appear twice. The GitHub username
44
+ * is read out of a noreply address, which is the only place git actually
45
+ * carries one; without it there is a name and no avatar, which is the truth.
46
+ */
47
+ export function contributorsOf(commits: DuxtCommitAuthor[]): DuxtContributor[] {
48
+ const people = new Map<string, DuxtContributor>();
49
+
50
+ for (const commit of commits) {
51
+ const key = commit.email.toLowerCase();
52
+ const existing = people.get(key);
53
+
54
+ if (existing) {
55
+ existing.commits += 1;
56
+ continue;
57
+ }
58
+
59
+ people.set(key, {
60
+ name: commit.name,
61
+ commits: 1,
62
+ username: githubUsername(commit.email)
63
+ });
64
+ }
65
+
66
+ return [...people.values()].sort((a, b) => b.commits - a.commits);
67
+ }
68
+
69
+ /** `1234567+octocat@users.noreply.github.com` becomes `octocat`. */
70
+ export function githubUsername(email: string): string | undefined {
71
+ const match = /^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i.exec(
72
+ email.trim()
73
+ );
74
+
75
+ return match?.[1];
76
+ }
77
+
78
+ /**
79
+ * An address GitHub itself issued to an APP rather than to a person.
80
+ *
81
+ * READ, NEVER GUESSED. GitHub writes `<id>+<app>[bot]@users.noreply.github.com`
82
+ * for every app installation, so the marker is the forge's own and not a
83
+ * taxonomy of ours — the same standard the changelog's group names are held to.
84
+ * The EMAIL is tested rather than the display name, because the email is
85
+ * already the identity above and a person may call themselves anything.
86
+ *
87
+ * Read by the RELEASE history and not by the page history, which is a scope
88
+ * line rather than an inconsistency: the issue this answers asks a release to
89
+ * name the people who wrote it, and this repository's own releases would
90
+ * otherwise each open with the bot that cut them. Whether "last touched by"
91
+ * should read the same way is a question about another surface.
92
+ */
93
+ export function isGitHubApp(author: DuxtCommitAuthor): boolean {
94
+ return /\[bot\]@users\.noreply\.github\.com$/i.test(author.email.trim());
95
+ }
96
+
97
+ /**
98
+ * The unit separator, not a comma or a pipe.
99
+ *
100
+ * An author name may hold anything a person is called and a decoration list is
101
+ * comma-separated prose; the one byte that cannot appear in either is one git
102
+ * will emit for us with `%x1f`.
103
+ */
104
+ const SEPARATOR = '\u001f';
105
+
106
+ /** The record separator, marking where one commit's line begins. */
107
+ const RECORD = '\u0001';
108
+
109
+ /** The fields one record carries, in order. */
110
+ const FORMAT = `${RECORD}%P${SEPARATOR}%an${SEPARATOR}%ae${SEPARATOR}%D`;
111
+
112
+ /** What git is asked, and the whole of why each flag is there. */
113
+ const ARGUMENTS = [
114
+ 'log',
115
+ // FROM THE TAGS, NOT FROM `HEAD`. A release tag is cut on the release branch,
116
+ // so on an integration branch — which is what every pull request builds — all
117
+ // but the oldest are unreachable and the history comes back empty. Walking
118
+ // the tags also excludes, for free, the commits no release carries yet:
119
+ // bucketing those into the newest tag would credit people for work that has
120
+ // not shipped.
121
+ '--tags',
122
+ // Keeps a merged branch's commits together instead of letting their commit
123
+ // dates interleave them with another line of history. It is what makes the
124
+ // walk below equal to one `rev-list <previous>..<tag>` per release without
125
+ // paying for one `git` process per release.
126
+ '--topo-order',
127
+ '--decorate=short',
128
+ '--decorate-refs=refs/tags/*',
129
+ `--format=${FORMAT}`
130
+ ];
131
+
132
+ /**
133
+ * Every release tag, with the people whose commits it carries.
134
+ *
135
+ * ONE `git log` FOR THE WHOLE HISTORY, walked newest-first: a commit carrying a
136
+ * tag OPENS that tag's release, and every commit after it in the walk belongs
137
+ * there until the next tag opens the release before it. That is exactly the
138
+ * range `<previous tag>..<tag>` names, and the alternative — a `git log` per
139
+ * release — is the cost `modules/git-meta.ts` already paid once: the work is
140
+ * never the history, it is spawning the processes.
141
+ *
142
+ * A MERGE COMMIT SETS THE BOUNDARY AND CREDITS NOBODY. It has to be walked,
143
+ * because release-please tags the merge commit its release pull request
144
+ * produced and skipping it would lose the boundary altogether; it introduces no
145
+ * change of its own, so its author did not write the diff the release carries.
146
+ * The page history arrives at the same answer by another route — `--name-status`
147
+ * prints nothing for a merge — which is why the two surfaces agree about it.
148
+ */
149
+ export function parseReleaseContributors(
150
+ output: string
151
+ ): Map<string, DuxtContributor[]> {
152
+ const commits = new Map<string, DuxtCommitAuthor[]>();
153
+ const contributors = new Map<string, DuxtContributor[]>();
154
+
155
+ /** The tags whose release the commits now being read belong to. */
156
+ let current: string[] = [];
157
+
158
+ for (const record of output.split(RECORD)) {
159
+ if (!record.trim()) continue;
160
+
161
+ const [parents, name, email, decoration] = record
162
+ .split('\n')[0]!
163
+ .split(SEPARATOR);
164
+
165
+ const tags = tagsOf(decoration ?? '');
166
+
167
+ if (tags.length) {
168
+ current = tags;
169
+ for (const tag of tags) commits.set(tag, commits.get(tag) ?? []);
170
+ }
171
+
172
+ if (!current.length) continue;
173
+
174
+ // A merge carries more than one parent. It opened the release above and
175
+ // says nothing else.
176
+ if ((parents ?? '').trim().includes(' ')) continue;
177
+
178
+ const author = { name: name ?? '', email: email ?? '' };
179
+ if (isGitHubApp(author)) continue;
180
+
181
+ for (const tag of current) commits.get(tag)!.push(author);
182
+ }
183
+
184
+ for (const [tag, authors] of commits) {
185
+ contributors.set(tag, contributorsOf(authors));
186
+ }
187
+
188
+ return contributors;
189
+ }
190
+
191
+ /**
192
+ * The tag names in one commit's decoration.
193
+ *
194
+ * Every tag on a commit names the same range of history, so all of them are
195
+ * kept: a project that tags `v1.0.0` and `1.0.0` at once is answered under
196
+ * whichever spelling its changelog happens to use.
197
+ */
198
+ function tagsOf(decoration: string): string[] {
199
+ return decoration
200
+ .split(',')
201
+ .map((entry) => entry.trim())
202
+ .filter((entry) => entry.startsWith('tag: '))
203
+ .map((entry) => entry.slice('tag: '.length).trim())
204
+ .filter(Boolean);
205
+ }
206
+
207
+ /**
208
+ * Two ways of writing a repository, reduced to the one thing that identifies it.
209
+ *
210
+ * `kirchDev/duxt` resolves to `https://github.com/kirchDev/duxt`, a checkout's
211
+ * remote is routinely `git@github.com:kirchDev/duxt.git`, and both name the same
212
+ * repository. Scheme, credentials, the scp-style colon, a trailing `.git` and
213
+ * case go; GitHub treats owner and name case-insensitively, and so does this.
214
+ */
215
+ export function sameRepository(a: string, b: string): boolean {
216
+ const key = (url: string) =>
217
+ url
218
+ .trim()
219
+ .replace(/^[a-z][a-z+.-]*:\/\//i, '')
220
+ .replace(/^[^@/]+@/, '')
221
+ .replace(/^([^/:]+):(?!\d+\/)/, '$1/')
222
+ .replace(/\.git\/?$/i, '')
223
+ .replace(/\/+$/, '')
224
+ .toLowerCase();
225
+
226
+ return key(a) === key(b);
227
+ }
228
+
229
+ /**
230
+ * The local checkout's history, where a DOWNLOADED source is this very
231
+ * repository.
232
+ *
233
+ * Content clones a remote source at `--depth 1`, and a history like that is
234
+ * wrong data rather than missing data — which is why a downloaded changelog
235
+ * reads no contributors. But a site very often names ITS OWN repository as a
236
+ * source, to publish the documentation of a tag rather than of the commit being
237
+ * built, and then the full history is already on disk: the tags are the same
238
+ * tags, so a release is the same range of commits whichever copy is asked.
239
+ *
240
+ * Every remote counts, not only `origin`: a fork's checkout calls the project
241
+ * `upstream`. A failure is silent for the reason `releaseContributors` gives —
242
+ * nothing was configured, so no git means what it meant before.
243
+ */
244
+ export function localHistoryFor(url: string, root: string): string | undefined {
245
+ try {
246
+ const remotes = execFileSync('git', ['-C', root, 'remote', '-v'], {
247
+ encoding: 'utf8',
248
+ stdio: ['ignore', 'pipe', 'ignore']
249
+ });
250
+
251
+ return remotes
252
+ .split('\n')
253
+ .map((line) => line.split(/\s+/)[1])
254
+ .some((remote) => remote && sameRepository(remote, url))
255
+ ? root
256
+ : undefined;
257
+ } catch {
258
+ return undefined;
259
+ }
260
+ }
261
+
262
+ /** One answer per repository, read the first time a section asks for it. */
263
+ const cache = new Map<string, Map<string, DuxtContributor[]>>();
264
+
265
+ /**
266
+ * The releases of the checkout at `root`, by tag.
267
+ *
268
+ * A FAILURE IS SILENT, and deliberately not the reporting `sources-git.ts`
269
+ * does: there, tags that cannot be read are a source a maintainer has to fix,
270
+ * and "found no tags" sent them to edit a source that was never wrong. Here
271
+ * nothing was configured at all — the history is read because it happens to be
272
+ * there — so no git, no checkout and a repository that has never been tagged
273
+ * all mean what they mean in `modules/git-meta.ts`: the page shows what it
274
+ * showed before this module existed.
275
+ */
276
+ export function releaseContributors(
277
+ root: string
278
+ ): Map<string, DuxtContributor[]> {
279
+ const cached = cache.get(root);
280
+ if (cached) return cached;
281
+
282
+ let found: Map<string, DuxtContributor[]>;
283
+
284
+ try {
285
+ found = parseReleaseContributors(
286
+ execFileSync('git', ['-C', root, ...ARGUMENTS], {
287
+ encoding: 'utf8',
288
+ stdio: ['ignore', 'pipe', 'ignore'],
289
+ // A repository's whole log against execFileSync's 1 MB default is a
290
+ // truncated history reported as a git failure.
291
+ maxBuffer: Number.POSITIVE_INFINITY
292
+ })
293
+ );
294
+ } catch {
295
+ found = new Map();
296
+ }
297
+
298
+ cache.set(root, found);
299
+ return found;
300
+ }
301
+
302
+ /**
303
+ * The people a changelog's version names, matched to the tag it was cut as.
304
+ *
305
+ * A changelog heading says `0.2.0` and the tag says `v0.2.0` — release-please's
306
+ * `include-v-in-tag` — so both spellings are tried, in both directions. Nothing
307
+ * is inferred beyond that: a version with no tag is a release this checkout
308
+ * cannot see (one cut after the build, or a hand-written fixture), and the
309
+ * honest answer for it is no contributors rather than someone else's.
310
+ */
311
+ export function contributorsForVersion(
312
+ releases: Map<string, DuxtContributor[]> | undefined,
313
+ version: string
314
+ ): DuxtContributor[] | undefined {
315
+ if (!releases) return undefined;
316
+
317
+ const bare = version.replace(/^v/, '');
318
+
319
+ return (
320
+ releases.get(version) ?? releases.get(`v${bare}`) ?? releases.get(bare)
321
+ );
322
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "duxt": {
3
+ "announcement": {
4
+ "dismiss": "Diese Ankündigung schließen"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "duxt": {
3
+ "bruno": {
4
+ "auth": "Authentifizierung",
5
+ "body": "Body",
6
+ "download": "Collection herunterladen",
7
+ "downloadHint": "Die Collection genau so, wie diese Version und Sprache sie beschreiben.",
8
+ "environments": "Umgebungen",
9
+ "fetch": "In Bruno öffnen",
10
+ "fetchHint": "Klont das Repository in Bruno. Es folgt dem Standard-Branch und ist nicht an diese Version gebunden.",
11
+ "folders": "Ordner",
12
+ "format": "Format",
13
+ "getIt": "Collection beziehen",
14
+ "hasCode": "Diese Anfrage enthält außerdem Skripte oder Tests. Sie werden hier weder angezeigt noch ausgeführt; im Download sind sie enthalten.",
15
+ "headers": "Header",
16
+ "name": "Name",
17
+ "notPublished": "Umgebungen, Zugangsdaten, Skripte und Tests werden auf dieser Seite nie veröffentlicht und nie im Browser ausgeführt.",
18
+ "parameters": "Parameter",
19
+ "requests": "Anfragen",
20
+ "shared": "Für alle Anfragen gemeinsam",
21
+ "value": "Wert",
22
+ "withheld": "zurückgehalten"
23
+ }
24
+ }
25
+ }
@@ -5,7 +5,7 @@
5
5
  "filter": "Nach Art der Änderung filtern",
6
6
  "latest": "Neueste",
7
7
  "changes": "Änderungen",
8
- "kinds": "Änderungsarten",
8
+ "contributors": "Mitwirkende",
9
9
  "last": "Letztes Release",
10
10
  "more": "und {count} weitere",
11
11
  "kind": "Änderungsart",
@@ -7,7 +7,8 @@
7
7
  "preview": "Vorschau",
8
8
  "source": "Code",
9
9
  "previewTabs": "Beispiel und Quelltext",
10
- "copiedToast": "In die Zwischenablage kopiert"
10
+ "copiedToast": "In die Zwischenablage kopiert",
11
+ "copyFailed": "Konnte nicht in die Zwischenablage kopieren."
11
12
  }
12
13
  }
13
14
  }
@@ -13,6 +13,15 @@
13
13
  "no": "Nein",
14
14
  "thanks": "Vielen Dank."
15
15
  },
16
+ "image": {
17
+ "close": "Bildansicht schließen",
18
+ "original": "Original in neuem Tab öffnen",
19
+ "resetZoom": "Zoom zurücksetzen",
20
+ "zoom": "Bild vergrößern",
21
+ "zoomIn": "Vergrößern",
22
+ "zoomNamed": "Bild vergrößern: {alt}",
23
+ "zoomOut": "Verkleinern"
24
+ },
16
25
  "tabs": "Varianten",
17
26
  "since": "Neu in {version}",
18
27
  "copy": {
@@ -2,6 +2,7 @@
2
2
  "duxt": {
3
3
  "shortcuts": {
4
4
  "title": "Tastenkürzel",
5
+ "open": "Tastenkürzel anzeigen",
5
6
  "search": "Dokumentation durchsuchen",
6
7
  "help": "Diese Liste anzeigen",
7
8
  "previous": "Vorherige Seite",
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "duxt": {
3
3
  "theme": {
4
- "toggle": "Erscheinungsbild wechseln"
4
+ "toLight": "Zum hellen Erscheinungsbild wechseln",
5
+ "toDark": "Zum dunklen Erscheinungsbild wechseln"
5
6
  }
6
7
  }
7
8
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "duxt": {
3
+ "announcement": {
4
+ "dismiss": "Dismiss this announcement"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "duxt": {
3
+ "bruno": {
4
+ "auth": "Authentication",
5
+ "body": "Body",
6
+ "download": "Download collection",
7
+ "downloadHint": "The collection exactly as this version and language describe it.",
8
+ "environments": "Environments",
9
+ "fetch": "Fetch in Bruno",
10
+ "fetchHint": "Clones the repository in Bruno. It follows the default branch and is not pinned to this version.",
11
+ "folders": "Folders",
12
+ "format": "Format",
13
+ "getIt": "Get the collection",
14
+ "hasCode": "This request also carries scripts or tests. They are neither shown here nor run; the download has them.",
15
+ "headers": "Headers",
16
+ "name": "Name",
17
+ "notPublished": "Environments, credentials, scripts and tests are never published on this site and never run in your browser.",
18
+ "parameters": "Parameters",
19
+ "requests": "Requests",
20
+ "shared": "Shared by every request",
21
+ "value": "Value",
22
+ "withheld": "withheld"
23
+ }
24
+ }
25
+ }
@@ -5,7 +5,7 @@
5
5
  "filter": "Filter by kind of change",
6
6
  "latest": "Latest",
7
7
  "changes": "Changes",
8
- "kinds": "Kinds of change",
8
+ "contributors": "Contributors",
9
9
  "last": "Last release",
10
10
  "more": "and {count} more",
11
11
  "kind": "Kind of change",
@@ -7,7 +7,8 @@
7
7
  "preview": "Preview",
8
8
  "source": "Code",
9
9
  "previewTabs": "Example and source",
10
- "copiedToast": "Copied to clipboard"
10
+ "copiedToast": "Copied to clipboard",
11
+ "copyFailed": "Could not copy to the clipboard."
11
12
  }
12
13
  }
13
14
  }
@@ -13,6 +13,15 @@
13
13
  "no": "No",
14
14
  "thanks": "Thank you."
15
15
  },
16
+ "image": {
17
+ "close": "Close image viewer",
18
+ "original": "Open original in a new tab",
19
+ "resetZoom": "Reset zoom",
20
+ "zoom": "Enlarge image",
21
+ "zoomIn": "Zoom in",
22
+ "zoomNamed": "Enlarge image: {alt}",
23
+ "zoomOut": "Zoom out"
24
+ },
16
25
  "tabs": "Options",
17
26
  "since": "New in {version}",
18
27
  "copy": {
@@ -2,6 +2,7 @@
2
2
  "duxt": {
3
3
  "shortcuts": {
4
4
  "title": "Keyboard shortcuts",
5
+ "open": "Show keyboard shortcuts",
5
6
  "search": "Search the documentation",
6
7
  "help": "Show this list",
7
8
  "previous": "Previous page",
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "duxt": {
3
3
  "theme": {
4
- "toggle": "Toggle theme"
4
+ "toLight": "Switch to light theme",
5
+ "toDark": "Switch to dark theme"
5
6
  }
6
7
  }
7
8
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "duxt": {
3
+ "announcement": {
4
+ "dismiss": "Descartar este anuncio"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "duxt": {
3
+ "bruno": {
4
+ "auth": "Autenticación",
5
+ "body": "Cuerpo",
6
+ "download": "Descargar la colección",
7
+ "downloadHint": "La colección tal y como la describen esta versión y este idioma.",
8
+ "environments": "Entornos",
9
+ "fetch": "Abrir en Bruno",
10
+ "fetchHint": "Clona el repositorio en Bruno. Sigue la rama por defecto y no está fijado a esta versión.",
11
+ "folders": "Carpetas",
12
+ "format": "Formato",
13
+ "getIt": "Obtener la colección",
14
+ "hasCode": "Esta petición también incluye scripts o pruebas. Aquí no se muestran ni se ejecutan; están en la descarga.",
15
+ "headers": "Cabeceras",
16
+ "name": "Nombre",
17
+ "notPublished": "Los entornos, las credenciales, los scripts y las pruebas nunca se publican en este sitio ni se ejecutan en tu navegador.",
18
+ "parameters": "Parámetros",
19
+ "requests": "Peticiones",
20
+ "shared": "Común a todas las peticiones",
21
+ "value": "Valor",
22
+ "withheld": "oculto"
23
+ }
24
+ }
25
+ }
@@ -5,7 +5,7 @@
5
5
  "filter": "Filtrar por tipo de cambio",
6
6
  "latest": "Más reciente",
7
7
  "changes": "Cambios",
8
- "kinds": "Tipos de cambio",
8
+ "contributors": "Colaboradores",
9
9
  "last": "Última versión",
10
10
  "more": "y {count} más",
11
11
  "kind": "Tipo de cambio",
@@ -7,7 +7,8 @@
7
7
  "preview": "Vista previa",
8
8
  "source": "Código",
9
9
  "previewTabs": "Ejemplo y código fuente",
10
- "copiedToast": "Copiado al portapapeles"
10
+ "copiedToast": "Copiado al portapapeles",
11
+ "copyFailed": "No se pudo copiar al portapapeles."
11
12
  }
12
13
  }
13
14
  }
@@ -13,6 +13,15 @@
13
13
  "no": "No",
14
14
  "thanks": "Gracias."
15
15
  },
16
+ "image": {
17
+ "close": "Cerrar el visor de imágenes",
18
+ "original": "Abrir el original en una pestaña nueva",
19
+ "resetZoom": "Restablecer el zoom",
20
+ "zoom": "Ampliar imagen",
21
+ "zoomIn": "Acercar",
22
+ "zoomNamed": "Ampliar imagen: {alt}",
23
+ "zoomOut": "Alejar"
24
+ },
16
25
  "tabs": "Opciones",
17
26
  "since": "Novedad en {version}",
18
27
  "copy": {
@@ -2,6 +2,7 @@
2
2
  "duxt": {
3
3
  "shortcuts": {
4
4
  "title": "Atajos de teclado",
5
+ "open": "Mostrar los atajos de teclado",
5
6
  "search": "Buscar en la documentación",
6
7
  "help": "Mostrar esta lista",
7
8
  "previous": "Página anterior",
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "duxt": {
3
3
  "theme": {
4
- "toggle": "Cambiar el tema"
4
+ "toLight": "Cambiar al tema claro",
5
+ "toDark": "Cambiar al tema oscuro"
5
6
  }
6
7
  }
7
8
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "duxt": {
3
+ "announcement": {
4
+ "dismiss": "Masquer cette annonce"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "duxt": {
3
+ "bruno": {
4
+ "auth": "Authentification",
5
+ "body": "Corps",
6
+ "download": "Télécharger la collection",
7
+ "downloadHint": "La collection telle que cette version et cette langue la décrivent.",
8
+ "environments": "Environnements",
9
+ "fetch": "Ouvrir dans Bruno",
10
+ "fetchHint": "Clone le dépôt dans Bruno. Il suit la branche par défaut et n'est pas figé sur cette version.",
11
+ "folders": "Dossiers",
12
+ "format": "Format",
13
+ "getIt": "Obtenir la collection",
14
+ "hasCode": "Cette requête comporte aussi des scripts ou des tests. Ils ne sont ni affichés ni exécutés ici ; le téléchargement les contient.",
15
+ "headers": "En-têtes",
16
+ "name": "Nom",
17
+ "notPublished": "Les environnements, les identifiants, les scripts et les tests ne sont jamais publiés sur ce site ni exécutés dans votre navigateur.",
18
+ "parameters": "Paramètres",
19
+ "requests": "Requêtes",
20
+ "shared": "Commun à toutes les requêtes",
21
+ "value": "Valeur",
22
+ "withheld": "masqué"
23
+ }
24
+ }
25
+ }
@@ -5,7 +5,7 @@
5
5
  "filter": "Filtrer par type de modification",
6
6
  "latest": "Plus récente",
7
7
  "changes": "Modifications",
8
- "kinds": "Types de modification",
8
+ "contributors": "Contributeurs",
9
9
  "last": "Dernière version",
10
10
  "more": "et {count} de plus",
11
11
  "kind": "Type de modification",
@@ -7,7 +7,8 @@
7
7
  "preview": "Aperçu",
8
8
  "source": "Code",
9
9
  "previewTabs": "Exemple et source",
10
- "copiedToast": "Copié dans le presse-papiers"
10
+ "copiedToast": "Copié dans le presse-papiers",
11
+ "copyFailed": "Impossible de copier dans le presse-papiers."
11
12
  }
12
13
  }
13
14
  }