@mannisto/astro-metadata 1.0.2 → 2.0.0-alpha.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/README.md +427 -82
- package/package.json +25 -14
- package/src/assets/icons/LICENSE.md +35 -0
- package/src/assets/icons/binoculars.svg +1 -0
- package/src/assets/icons/caret-right.svg +1 -0
- package/src/assets/icons/check.svg +1 -0
- package/src/assets/icons/copy.svg +1 -0
- package/src/assets/icons/seal-check-duotone.svg +1 -0
- package/src/assets/icons/seal-question-duotone.svg +1 -0
- package/src/assets/icons/warning-circle-duotone.svg +1 -0
- package/src/assets/icons/warning-duotone.svg +1 -0
- package/src/assets/icons/x.svg +1 -0
- package/src/components/Metadata.astro +145 -0
- package/src/components/index.ts +2 -0
- package/src/integration.d.ts +112 -0
- package/src/integration.js +508 -0
- package/src/lib/checks.d.ts +70 -0
- package/src/lib/checks.js +193 -0
- package/src/lib/config.ts +111 -0
- package/src/lib/favicon-files.d.ts +29 -0
- package/src/lib/favicon-files.js +103 -0
- package/src/lib/favicon-image.ts +124 -0
- package/src/lib/favicon.ts +167 -0
- package/src/lib/head.d.ts +25 -0
- package/src/lib/head.js +141 -0
- package/src/lib/manifest.ts +126 -0
- package/src/lib/rules.d.ts +73 -0
- package/src/lib/rules.js +902 -0
- package/src/lib/schema.ts +208 -0
- package/src/lib/site.ts +72 -0
- package/src/lib/url.ts +79 -0
- package/src/routes/favicon.ts +14 -0
- package/src/routes/manifest.ts +18 -0
- package/src/runtime.ts +16 -0
- package/src/toolbar/app.ts +72 -0
- package/src/toolbar/assets.ts +81 -0
- package/src/toolbar/data.ts +282 -0
- package/src/toolbar/define.ts +7 -0
- package/src/toolbar/env.d.ts +10 -0
- package/src/toolbar/highlight.ts +83 -0
- package/src/toolbar/styles/shared.css +18 -0
- package/src/toolbar/styles/tokens.css +182 -0
- package/src/toolbar/toolbar-badge.ts +78 -0
- package/src/toolbar/ui/alert/alert.css +126 -0
- package/src/toolbar/ui/alert/alert.ts +102 -0
- package/src/toolbar/ui/badge/badge.css +41 -0
- package/src/toolbar/ui/badge/badge.ts +24 -0
- package/src/toolbar/ui/code-block/code-block.css +90 -0
- package/src/toolbar/ui/code-block/code-block.ts +68 -0
- package/src/toolbar/ui/color-swatch/color-swatch.css +15 -0
- package/src/toolbar/ui/color-swatch/color-swatch.ts +31 -0
- package/src/toolbar/ui/icon/icon.css +11 -0
- package/src/toolbar/ui/icon/icon.ts +46 -0
- package/src/toolbar/ui/image/image.css +18 -0
- package/src/toolbar/ui/image/image.ts +58 -0
- package/src/toolbar/ui/inline-code/inline-code.css +16 -0
- package/src/toolbar/ui/inline-code/inline-code.ts +41 -0
- package/src/toolbar/ui/section/section.css +88 -0
- package/src/toolbar/ui/section/section.ts +78 -0
- package/src/toolbar/ui/tab/tab.css +7 -0
- package/src/toolbar/ui/tab/tab.ts +47 -0
- package/src/toolbar/ui/tab-group/tab-group.css +42 -0
- package/src/toolbar/ui/tab-group/tab-group.ts +101 -0
- package/src/toolbar/ui/table/table.css +124 -0
- package/src/toolbar/ui/table/table.ts +155 -0
- package/src/toolbar/views/code-blocks.ts +35 -0
- package/src/toolbar/views/metadata-panel.css +101 -0
- package/src/toolbar/views/metadata-panel.ts +125 -0
- package/src/toolbar/views/overview-view.css +27 -0
- package/src/toolbar/views/overview-view.ts +401 -0
- package/src/toolbar/views/previews/discord.css +70 -0
- package/src/toolbar/views/previews/discord.ts +60 -0
- package/src/toolbar/views/previews/facebook.css +65 -0
- package/src/toolbar/views/previews/facebook.ts +36 -0
- package/src/toolbar/views/previews/google.css +75 -0
- package/src/toolbar/views/previews/google.ts +36 -0
- package/src/toolbar/views/previews/linkedin.css +58 -0
- package/src/toolbar/views/previews/linkedin.ts +34 -0
- package/src/toolbar/views/previews/page.ts +129 -0
- package/src/toolbar/views/previews/platform.ts +14 -0
- package/src/toolbar/views/previews/previews-view.css +37 -0
- package/src/toolbar/views/previews/previews-view.ts +52 -0
- package/src/toolbar/views/previews/slack.css +61 -0
- package/src/toolbar/views/previews/slack.ts +47 -0
- package/src/toolbar/views/previews/twitter.css +89 -0
- package/src/toolbar/views/previews/twitter.ts +57 -0
- package/src/toolbar/views/previews/whatsapp.css +88 -0
- package/src/toolbar/views/previews/whatsapp.ts +65 -0
- package/src/toolbar/views/raw-view.ts +41 -0
- package/index.ts +0 -31
- package/src/components/Canonical.astro +0 -11
- package/src/components/Description.astro +0 -9
- package/src/components/Favicon.astro +0 -96
- package/src/components/Head.astro +0 -94
- package/src/components/Keywords.astro +0 -9
- package/src/components/LanguageAlternates.astro +0 -18
- package/src/components/OpenGraph.astro +0 -75
- package/src/components/Robots.astro +0 -25
- package/src/components/Schema.astro +0 -18
- package/src/components/Title.astro +0 -13
- package/src/components/Twitter.astro +0 -27
- package/src/lib/metadata.ts +0 -77
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { parseHead } from "../lib/checks.js"
|
|
2
|
+
import type { Check } from "../lib/checks.js"
|
|
3
|
+
import { getConfig } from "../lib/config.ts"
|
|
4
|
+
import { loadAssets } from "./assets.ts"
|
|
5
|
+
|
|
6
|
+
export type { Check }
|
|
7
|
+
|
|
8
|
+
/** A meta tag: its `name` or `property`, and its `content`. */
|
|
9
|
+
export type MetaTag = {
|
|
10
|
+
key: string
|
|
11
|
+
value: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A link tag and its attributes. */
|
|
15
|
+
export type LinkTag = {
|
|
16
|
+
rel: string
|
|
17
|
+
href: string
|
|
18
|
+
type?: string
|
|
19
|
+
sizes?: string
|
|
20
|
+
hreflang?: string
|
|
21
|
+
title?: string
|
|
22
|
+
media?: string
|
|
23
|
+
/** The tag as HTML. */
|
|
24
|
+
html: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A JSON-LD script: its source, and its data when the JSON is valid. */
|
|
28
|
+
export type JsonLd = {
|
|
29
|
+
source: string
|
|
30
|
+
data?: unknown
|
|
31
|
+
/** The `@type` values, e.g. ["WebSite", "BreadcrumbList"]. */
|
|
32
|
+
types: string[]
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The web app manifest of the page, loaded from its URL. */
|
|
37
|
+
export type ManifestData = {
|
|
38
|
+
url: string
|
|
39
|
+
data?: unknown
|
|
40
|
+
error?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** All metadata of the current page. The tabs of the panel only read this object. */
|
|
44
|
+
export type PageMetadata = {
|
|
45
|
+
url: string
|
|
46
|
+
lang?: string
|
|
47
|
+
title?: string
|
|
48
|
+
description?: string
|
|
49
|
+
canonical?: string
|
|
50
|
+
robots?: string
|
|
51
|
+
themeColors: { color: string; media?: string }[]
|
|
52
|
+
openGraph: MetaTag[]
|
|
53
|
+
twitter: MetaTag[]
|
|
54
|
+
icons: LinkTag[]
|
|
55
|
+
feeds: LinkTag[]
|
|
56
|
+
hreflang: LinkTag[]
|
|
57
|
+
manifest?: ManifestData
|
|
58
|
+
schemas: JsonLd[]
|
|
59
|
+
/** True for a page that the `pages.ignore` option matches: it gets no checks. */
|
|
60
|
+
ignored: boolean
|
|
61
|
+
/** The metadata tags of the head as HTML, one tag on each line. */
|
|
62
|
+
source: string
|
|
63
|
+
checks: Check[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The link tags of the metadata. Other links, e.g. stylesheets, are not metadata.
|
|
67
|
+
const METADATA_LINKS = ["canonical", "icon", "apple-touch-icon", "manifest", "alternate"]
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the attributes of a link tag.
|
|
71
|
+
*/
|
|
72
|
+
function linkTag(element: HTMLLinkElement): LinkTag {
|
|
73
|
+
return {
|
|
74
|
+
rel: element.rel,
|
|
75
|
+
href: element.getAttribute("href") ?? "",
|
|
76
|
+
type: element.getAttribute("type") ?? undefined,
|
|
77
|
+
sizes: element.getAttribute("sizes") ?? undefined,
|
|
78
|
+
hreflang: element.getAttribute("hreflang") ?? undefined,
|
|
79
|
+
title: element.getAttribute("title") ?? undefined,
|
|
80
|
+
media: element.getAttribute("media") ?? undefined,
|
|
81
|
+
html: element.outerHTML,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the meta tags with a `name` or `property` that starts with a prefix, in their order.
|
|
87
|
+
*/
|
|
88
|
+
function metaTags(prefix: string): MetaTag[] {
|
|
89
|
+
const tags = [...document.head.querySelectorAll("meta")]
|
|
90
|
+
return tags.flatMap((element) => {
|
|
91
|
+
const key = element.getAttribute("property") ?? element.getAttribute("name") ?? ""
|
|
92
|
+
return key.startsWith(prefix) ? [{ key, value: element.content }] : []
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns the content of the first meta tag with a name.
|
|
98
|
+
*/
|
|
99
|
+
function metaContent(name: string): string | undefined {
|
|
100
|
+
return document.head.querySelector<HTMLMetaElement>(`meta[name="${name}"]`)?.content
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the JSON-LD scripts of the page, in the head and in the body.
|
|
105
|
+
*/
|
|
106
|
+
function jsonLd(): JsonLd[] {
|
|
107
|
+
const scripts = [...document.querySelectorAll('script[type="application/ld+json"]')]
|
|
108
|
+
return scripts.map((script) => {
|
|
109
|
+
const source = script.textContent ?? ""
|
|
110
|
+
try {
|
|
111
|
+
const data = JSON.parse(source)
|
|
112
|
+
const items = Array.isArray(data) ? data : [data]
|
|
113
|
+
const types = items.map((item) => {
|
|
114
|
+
return String(item?.["@type"] ?? "?")
|
|
115
|
+
})
|
|
116
|
+
return { source, data, types }
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return { source, types: [], error: error instanceof Error ? error.message : String(error) }
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns the metadata tags of the head as HTML. The scripts and styles of the dev server
|
|
125
|
+
* are left out, so the source shows only what the site renders.
|
|
126
|
+
*/
|
|
127
|
+
function headSource(): string {
|
|
128
|
+
const elements = [...document.head.children].filter((element) => {
|
|
129
|
+
const tag = element.tagName.toLowerCase()
|
|
130
|
+
if (tag === "title" || tag === "meta") return true
|
|
131
|
+
if (tag === "link") return METADATA_LINKS.includes((element as HTMLLinkElement).rel)
|
|
132
|
+
return tag === "script" && element.getAttribute("type") === "application/ld+json"
|
|
133
|
+
})
|
|
134
|
+
return elements
|
|
135
|
+
.map((element) => {
|
|
136
|
+
return element.outerHTML
|
|
137
|
+
})
|
|
138
|
+
.join("\n")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Loads the web app manifest of the page.
|
|
143
|
+
*/
|
|
144
|
+
async function loadManifest(url: string): Promise<ManifestData> {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(url)
|
|
147
|
+
if (!response.ok) return { url, error: `The server answered ${response.status}.` }
|
|
148
|
+
return { url, data: await response.json() }
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return { url, error: error instanceof Error ? error.message : String(error) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Reads the metadata of the current page. The checks are the same as the build checks.
|
|
156
|
+
*/
|
|
157
|
+
export async function collect(): Promise<PageMetadata> {
|
|
158
|
+
const head = document.head
|
|
159
|
+
const links = [...head.querySelectorAll<HTMLLinkElement>("link")].map(linkTag)
|
|
160
|
+
const manifestUrl = links.find((link) => {
|
|
161
|
+
return link.rel === "manifest"
|
|
162
|
+
})?.href
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
url: location.href,
|
|
166
|
+
lang: document.documentElement.lang || undefined,
|
|
167
|
+
title: head.querySelector("title")?.textContent ?? undefined,
|
|
168
|
+
description: metaContent("description"),
|
|
169
|
+
canonical: links.find((link) => {
|
|
170
|
+
return link.rel === "canonical"
|
|
171
|
+
})?.href,
|
|
172
|
+
robots: metaContent("robots"),
|
|
173
|
+
themeColors: [...head.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')].map(
|
|
174
|
+
(element) => {
|
|
175
|
+
return { color: element.content, media: element.media || undefined }
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
openGraph: metaTags("og:"),
|
|
179
|
+
twitter: metaTags("twitter:"),
|
|
180
|
+
icons: links.filter((link) => {
|
|
181
|
+
return link.rel === "icon" || link.rel === "apple-touch-icon"
|
|
182
|
+
}),
|
|
183
|
+
feeds: links.filter((link) => {
|
|
184
|
+
return link.rel === "alternate" && !link.hreflang && Boolean(link.type)
|
|
185
|
+
}),
|
|
186
|
+
hreflang: links.filter((link) => {
|
|
187
|
+
return link.rel === "alternate" && Boolean(link.hreflang)
|
|
188
|
+
}),
|
|
189
|
+
manifest: manifestUrl ? await loadManifest(manifestUrl) : undefined,
|
|
190
|
+
schemas: jsonLd(),
|
|
191
|
+
source: headSource(),
|
|
192
|
+
...(await checkPage()),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns the value of a meta tag of a list, e.g. "og:image" of the Open Graph tags.
|
|
198
|
+
*/
|
|
199
|
+
export function findMeta(tags: MetaTag[], key: string): string | undefined {
|
|
200
|
+
return tags.find((tag) => {
|
|
201
|
+
return tag.key === key
|
|
202
|
+
})?.value
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The problems of a page: the number of errors and warnings, and the worst level. */
|
|
206
|
+
export type CheckSummary = {
|
|
207
|
+
errors: number
|
|
208
|
+
warnings: number
|
|
209
|
+
/** "error" or "warning", or undefined for a page without problems. */
|
|
210
|
+
level?: "error" | "warning"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Counts the errors and the warnings. Information, e.g. noindex, is not a problem.
|
|
215
|
+
* The toolbar badge, the tab badge and the status alert use the same summary.
|
|
216
|
+
*/
|
|
217
|
+
export function summarize(checks: Check[]): CheckSummary {
|
|
218
|
+
const errors = checks.filter((check) => {
|
|
219
|
+
return check.level === "error"
|
|
220
|
+
}).length
|
|
221
|
+
const warnings = checks.filter((check) => {
|
|
222
|
+
return check.level === "warning"
|
|
223
|
+
}).length
|
|
224
|
+
|
|
225
|
+
let level: CheckSummary["level"]
|
|
226
|
+
if (errors > 0) level = "error"
|
|
227
|
+
else if (warnings > 0) level = "warning"
|
|
228
|
+
return { errors, warnings, level }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The result of the checks of a page. */
|
|
232
|
+
export type PageChecks = {
|
|
233
|
+
/** True for a page that the `pages.ignore` option matches: it gets no checks. */
|
|
234
|
+
ignored: boolean
|
|
235
|
+
checks: Check[]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The check endpoint of the dev server. The integration adds it.
|
|
239
|
+
const CHECK_PATH = "/__astro-metadata/check"
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns the path of the current page without the base, e.g. "/blog/" for "/docs/blog/".
|
|
243
|
+
*/
|
|
244
|
+
function pagePath(): string {
|
|
245
|
+
const prefix = (getConfig().base ?? "/").replace(/\/$/, "")
|
|
246
|
+
const path = location.pathname
|
|
247
|
+
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Checks the current page with the same rules as the build. The app loads the files of the
|
|
252
|
+
* page, and the dev server runs the rules: the custom rules are functions in the Astro
|
|
253
|
+
* config, so only the server has them.
|
|
254
|
+
*/
|
|
255
|
+
export async function checkPage(): Promise<PageChecks> {
|
|
256
|
+
const html = document.documentElement.outerHTML
|
|
257
|
+
const assets = await loadAssets(parseHead(html))
|
|
258
|
+
const prefix = (getConfig().base ?? "/").replace(/\/$/, "")
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const response = await fetch(`${prefix}${CHECK_PATH}`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "Content-Type": "application/json" },
|
|
264
|
+
body: JSON.stringify({ html, path: pagePath(), assets }),
|
|
265
|
+
})
|
|
266
|
+
if (!response.ok) throw new Error(`The server answered ${response.status}.`)
|
|
267
|
+
return (await response.json()) as PageChecks
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
270
|
+
return {
|
|
271
|
+
ignored: false,
|
|
272
|
+
checks: [
|
|
273
|
+
{
|
|
274
|
+
id: "check-failed",
|
|
275
|
+
field: "custom",
|
|
276
|
+
level: "error",
|
|
277
|
+
message: `The dev server could not check the page: ${reason}`,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers a custom element one time. Hot reload runs the module again, and a second
|
|
3
|
+
* `customElements.define()` with the same name throws an error.
|
|
4
|
+
*/
|
|
5
|
+
export function define(name: string, element: CustomElementConstructor): void {
|
|
6
|
+
if (!customElements.get(name)) customElements.define(name, element)
|
|
7
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A small highlighter for the two formats of the panel: JSON and HTML.
|
|
2
|
+
// It wraps each token in a span with a class, and the CSS sets the colors.
|
|
3
|
+
|
|
4
|
+
export type Language = "json" | "html"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Escapes a text for use in HTML.
|
|
8
|
+
*/
|
|
9
|
+
export function escapeHtml(text: string): string {
|
|
10
|
+
return text
|
|
11
|
+
.replace(/&/g, "&")
|
|
12
|
+
.replace(/</g, "<")
|
|
13
|
+
.replace(/>/g, ">")
|
|
14
|
+
.replace(/"/g, """)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns a token in a span with a class.
|
|
19
|
+
*/
|
|
20
|
+
function token(name: string, text: string): string {
|
|
21
|
+
return `<span class="token ${name}">${escapeHtml(text)}</span>`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A string (and a colon after a key), a keyword, or a number
|
|
25
|
+
const JSON_TOKENS =
|
|
26
|
+
/("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g
|
|
27
|
+
|
|
28
|
+
// A tag start or end, an attribute with its value, or the end of a tag
|
|
29
|
+
const HTML_TOKENS = /(<\/?)([\w:-]+)|([\w:-]+)(=)("[^"]*"|'[^']*')|(\/?>)/g
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Highlights JSON.
|
|
33
|
+
*/
|
|
34
|
+
function highlightJson(code: string): string {
|
|
35
|
+
let output = ""
|
|
36
|
+
let last = 0
|
|
37
|
+
for (const match of code.matchAll(JSON_TOKENS)) {
|
|
38
|
+
output += escapeHtml(code.slice(last, match.index))
|
|
39
|
+
const [text, string, colon, keyword] = match
|
|
40
|
+
if (string && colon) {
|
|
41
|
+
output += token("key", string) + escapeHtml(colon)
|
|
42
|
+
} else if (string) {
|
|
43
|
+
output += token("string", string)
|
|
44
|
+
} else if (keyword) {
|
|
45
|
+
output += token("keyword", keyword)
|
|
46
|
+
} else {
|
|
47
|
+
output += token("number", text)
|
|
48
|
+
}
|
|
49
|
+
last = match.index + text.length
|
|
50
|
+
}
|
|
51
|
+
return output + escapeHtml(code.slice(last))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Highlights HTML tags and their attributes.
|
|
56
|
+
*/
|
|
57
|
+
function highlightHtml(code: string): string {
|
|
58
|
+
let output = ""
|
|
59
|
+
let last = 0
|
|
60
|
+
for (const match of code.matchAll(HTML_TOKENS)) {
|
|
61
|
+
output += escapeHtml(code.slice(last, match.index))
|
|
62
|
+
const [text, open, tag, name, equals, value, close] = match
|
|
63
|
+
if (tag) {
|
|
64
|
+
output += token("punctuation", open) + token("tag", tag)
|
|
65
|
+
} else if (name) {
|
|
66
|
+
output += token("attribute", name) + token("punctuation", equals) + token("value", value)
|
|
67
|
+
} else {
|
|
68
|
+
output += token("punctuation", close)
|
|
69
|
+
}
|
|
70
|
+
last = match.index + text.length
|
|
71
|
+
}
|
|
72
|
+
return output + escapeHtml(code.slice(last))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the code as HTML with a span for each token.
|
|
77
|
+
*
|
|
78
|
+
* @param code - The source code.
|
|
79
|
+
* @param language - "json" or "html".
|
|
80
|
+
*/
|
|
81
|
+
export function highlight(code: string, language: Language): string {
|
|
82
|
+
return language === "json" ? highlightJson(code) : highlightHtml(code)
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* The styles that all views share. The ui components bring their own styles. */
|
|
2
|
+
:host {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: minmax(0, 1fr);
|
|
5
|
+
gap: var(--space-300);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
p {
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.empty {
|
|
17
|
+
color: var(--zinc-400);
|
|
18
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/* The design values of the toolbar app. All components inherit them. */
|
|
2
|
+
:host {
|
|
3
|
+
all: initial;
|
|
4
|
+
|
|
5
|
+
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
6
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
7
|
+
--line-height: 1.5;
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The spacing and font size tokens of the other projects. They use rem with a root
|
|
11
|
+
* size of 62.5 % (1 rem = 10 px). In the toolbar, rem uses the root size of the user's
|
|
12
|
+
* site, so the values here are in px: 1.6rem is 16px.
|
|
13
|
+
*/
|
|
14
|
+
--space-0: 0;
|
|
15
|
+
--space-25: 1px;
|
|
16
|
+
--space-50: 2px;
|
|
17
|
+
--space-100: 4px;
|
|
18
|
+
--space-150: 6px;
|
|
19
|
+
--space-200: 8px;
|
|
20
|
+
--space-300: 12px;
|
|
21
|
+
--space-400: 16px;
|
|
22
|
+
--space-500: 20px;
|
|
23
|
+
--space-600: 24px;
|
|
24
|
+
--space-700: 28px;
|
|
25
|
+
--space-800: 32px;
|
|
26
|
+
--space-900: 36px;
|
|
27
|
+
--space-1000: 40px;
|
|
28
|
+
--space-1200: 48px;
|
|
29
|
+
--space-1600: 64px;
|
|
30
|
+
--space-2000: 80px;
|
|
31
|
+
--space-2400: 96px;
|
|
32
|
+
--space-2800: 112px;
|
|
33
|
+
--space-3200: 128px;
|
|
34
|
+
|
|
35
|
+
--font-size-tiny: 10.24px;
|
|
36
|
+
--font-size-small: 12.8px;
|
|
37
|
+
--font-size-body: 16px;
|
|
38
|
+
--font-size-h6: 20px;
|
|
39
|
+
--font-size-h5: 25px;
|
|
40
|
+
--font-size-h4: 31.25px;
|
|
41
|
+
--font-size-h3: 39.06px;
|
|
42
|
+
--font-size-h2: 48.83px;
|
|
43
|
+
--font-size-h1: 61.04px;
|
|
44
|
+
--font-size-display: 94px;
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* The color scales: zinc, red, orange, yellow, green and blue, in steps of 50 from
|
|
48
|
+
* 50 to 950. The values are the same as in the color tokens of the other projects.
|
|
49
|
+
* All colors of the toolbar app come from these scales.
|
|
50
|
+
*/
|
|
51
|
+
--zinc-50: oklch(98.51% 0 0deg);
|
|
52
|
+
--zinc-100: oklch(96.74% 0.0013 286.4deg);
|
|
53
|
+
--zinc-150: oklch(94.37% 0.0027 286.3deg);
|
|
54
|
+
--zinc-200: oklch(91.97% 0.004 286.3deg);
|
|
55
|
+
--zinc-250: oklch(89.56% 0.0054 286.3deg);
|
|
56
|
+
--zinc-300: oklch(87.11% 0.0055 286.3deg);
|
|
57
|
+
--zinc-350: oklch(79.39% 0.0084 286.2deg);
|
|
58
|
+
--zinc-400: oklch(71.18% 0.0129 286.1deg);
|
|
59
|
+
--zinc-450: oklch(63.3% 0.0133 286deg);
|
|
60
|
+
--zinc-500: oklch(55.17% 0.0138 285.9deg);
|
|
61
|
+
--zinc-550: oklch(49.93% 0.0142 285.9deg);
|
|
62
|
+
--zinc-600: oklch(44.19% 0.0146 285.8deg);
|
|
63
|
+
--zinc-650: oklch(40.83% 0.0133 285.8deg);
|
|
64
|
+
--zinc-700: oklch(37.03% 0.0119 285.8deg);
|
|
65
|
+
--zinc-750: oklch(32.3% 0.0088 285.9deg);
|
|
66
|
+
--zinc-800: oklch(27.39% 0.0055 286deg);
|
|
67
|
+
--zinc-850: oklch(24.48% 0.0056 286deg);
|
|
68
|
+
--zinc-900: oklch(21.03% 0.0059 285.9deg);
|
|
69
|
+
--zinc-950: oklch(14.08% 0.0044 285.8deg);
|
|
70
|
+
|
|
71
|
+
--red-50: oklch(97.05% 0.0129 17.4deg);
|
|
72
|
+
--red-100: oklch(93.56% 0.0309 17.7deg);
|
|
73
|
+
--red-150: oklch(90.99% 0.0449 18deg);
|
|
74
|
+
--red-200: oklch(88.45% 0.0593 18.3deg);
|
|
75
|
+
--red-250: oklch(84.65% 0.0804 18.9deg);
|
|
76
|
+
--red-300: oklch(80.77% 0.1035 19.6deg);
|
|
77
|
+
--red-350: oklch(75.69% 0.135 20.7deg);
|
|
78
|
+
--red-400: oklch(71.06% 0.1661 22.2deg);
|
|
79
|
+
--red-450: oklch(67.27% 0.1884 23.7deg);
|
|
80
|
+
--red-500: oklch(63.68% 0.2078 25.3deg);
|
|
81
|
+
--red-550: oklch(60.68% 0.2129 26.3deg);
|
|
82
|
+
--red-600: oklch(57.71% 0.2152 27.3deg);
|
|
83
|
+
--red-650: oklch(54.25% 0.2034 27.4deg);
|
|
84
|
+
--red-700: oklch(50.54% 0.1905 27.5deg);
|
|
85
|
+
--red-750: oklch(47.51% 0.1758 27.2deg);
|
|
86
|
+
--red-800: oklch(44.37% 0.1613 26.9deg);
|
|
87
|
+
--red-850: oklch(41.96% 0.1476 26.4deg);
|
|
88
|
+
--red-900: oklch(39.58% 0.1331 25.7deg);
|
|
89
|
+
--red-950: oklch(25.75% 0.0886 26deg);
|
|
90
|
+
|
|
91
|
+
--orange-50: oklch(97.96% 0.0158 73.7deg);
|
|
92
|
+
--orange-100: oklch(95.42% 0.0372 75.2deg);
|
|
93
|
+
--orange-150: oklch(92.81% 0.0549 71.5deg);
|
|
94
|
+
--orange-200: oklch(90.15% 0.0729 70.7deg);
|
|
95
|
+
--orange-250: oklch(86.98% 0.0955 68.6deg);
|
|
96
|
+
--orange-300: oklch(83.66% 0.1165 66.3deg);
|
|
97
|
+
--orange-350: oklch(79.58% 0.1381 60.8deg);
|
|
98
|
+
--orange-400: oklch(75.76% 0.159 55.9deg);
|
|
99
|
+
--orange-450: oklch(73.1% 0.1729 52deg);
|
|
100
|
+
--orange-500: oklch(70.49% 0.1867 47.6deg);
|
|
101
|
+
--orange-550: oklch(67.65% 0.1905 44.3deg);
|
|
102
|
+
--orange-600: oklch(64.61% 0.1943 41.1deg);
|
|
103
|
+
--orange-650: oklch(60.07% 0.1838 40deg);
|
|
104
|
+
--orange-700: oklch(55.34% 0.1739 38.4deg);
|
|
105
|
+
--orange-750: oklch(51.26% 0.1583 38.2deg);
|
|
106
|
+
--orange-800: oklch(46.98% 0.143 37.3deg);
|
|
107
|
+
--orange-850: oklch(43.99% 0.1295 38deg);
|
|
108
|
+
--orange-900: oklch(40.84% 0.1165 38.2deg);
|
|
109
|
+
--orange-950: oklch(26.59% 0.0762 36.3deg);
|
|
110
|
+
|
|
111
|
+
--yellow-50: oklch(98.73% 0.0262 102.2deg);
|
|
112
|
+
--yellow-100: oklch(97.29% 0.0693 103.2deg);
|
|
113
|
+
--yellow-150: oklch(95.96% 0.0981 102.4deg);
|
|
114
|
+
--yellow-200: oklch(94.51% 0.1243 101.5deg);
|
|
115
|
+
--yellow-250: oklch(92.5% 0.1477 99.6deg);
|
|
116
|
+
--yellow-300: oklch(90.52% 0.1657 98.1deg);
|
|
117
|
+
--yellow-350: oklch(88.3% 0.1714 95deg);
|
|
118
|
+
--yellow-400: oklch(86.06% 0.1731 91.9deg);
|
|
119
|
+
--yellow-450: oklch(82.89% 0.1674 89.4deg);
|
|
120
|
+
--yellow-500: oklch(79.52% 0.1617 86deg);
|
|
121
|
+
--yellow-550: oklch(73.92% 0.1515 81.7deg);
|
|
122
|
+
--yellow-600: oklch(68.06% 0.1423 75.8deg);
|
|
123
|
+
--yellow-650: oklch(61.85% 0.1314 71.4deg);
|
|
124
|
+
--yellow-700: oklch(55.38% 0.1207 66.4deg);
|
|
125
|
+
--yellow-750: oklch(51.63% 0.1119 64.9deg);
|
|
126
|
+
--yellow-800: oklch(47.62% 0.1034 61.9deg);
|
|
127
|
+
--yellow-850: oklch(44.88% 0.0967 60.1deg);
|
|
128
|
+
--yellow-900: oklch(42.1% 0.0897 57.7deg);
|
|
129
|
+
--yellow-950: oklch(28.57% 0.0639 53.8deg);
|
|
130
|
+
|
|
131
|
+
--green-50: oklch(98.19% 0.0181 155.8deg);
|
|
132
|
+
--green-100: oklch(96.24% 0.0434 156.7deg);
|
|
133
|
+
--green-150: oklch(94.49% 0.062 156.4deg);
|
|
134
|
+
--green-200: oklch(92.5% 0.0806 156deg);
|
|
135
|
+
--green-250: oklch(89.74% 0.1088 155.2deg);
|
|
136
|
+
--green-300: oklch(87.12% 0.1363 154.4deg);
|
|
137
|
+
--green-350: oklch(83.57% 0.1613 153.1deg);
|
|
138
|
+
--green-400: oklch(80.03% 0.1821 151.7deg);
|
|
139
|
+
--green-450: oklch(76.25% 0.1884 150.7deg);
|
|
140
|
+
--green-500: oklch(72.27% 0.192 149.6deg);
|
|
141
|
+
--green-550: oklch(67.53% 0.1811 149.4deg);
|
|
142
|
+
--green-600: oklch(62.71% 0.1699 149.2deg);
|
|
143
|
+
--green-650: oklch(57.92% 0.1539 149.6deg);
|
|
144
|
+
--green-700: oklch(52.73% 0.1371 150.1deg);
|
|
145
|
+
--green-750: oklch(48.96% 0.1231 150.7deg);
|
|
146
|
+
--green-800: oklch(44.79% 0.1083 151.3deg);
|
|
147
|
+
--green-850: oklch(42.05% 0.0986 152.1deg);
|
|
148
|
+
--green-900: oklch(39.25% 0.0896 152.5deg);
|
|
149
|
+
--green-950: oklch(26.64% 0.0628 152.9deg);
|
|
150
|
+
|
|
151
|
+
--blue-50: oklch(97.05% 0.0142 254.6deg);
|
|
152
|
+
--blue-100: oklch(93.19% 0.0316 255.6deg);
|
|
153
|
+
--blue-150: oklch(90.81% 0.0439 253.6deg);
|
|
154
|
+
--blue-200: oklch(88.23% 0.0571 254.1deg);
|
|
155
|
+
--blue-250: oklch(84.56% 0.0769 252.9deg);
|
|
156
|
+
--blue-300: oklch(80.91% 0.0956 251.8deg);
|
|
157
|
+
--blue-350: oklch(76.12% 0.1198 253.8deg);
|
|
158
|
+
--blue-400: oklch(71.37% 0.1434 254.6deg);
|
|
159
|
+
--blue-450: oklch(66.89% 0.1648 257.6deg);
|
|
160
|
+
--blue-500: oklch(62.31% 0.188 259.8deg);
|
|
161
|
+
--blue-550: oklch(58.54% 0.2017 261.5deg);
|
|
162
|
+
--blue-600: oklch(54.61% 0.2152 262.9deg);
|
|
163
|
+
--blue-650: oklch(51.85% 0.2163 263.6deg);
|
|
164
|
+
--blue-700: oklch(48.82% 0.2172 264.4deg);
|
|
165
|
+
--blue-750: oklch(45.71% 0.1998 265deg);
|
|
166
|
+
--blue-800: oklch(42.44% 0.1809 265.6deg);
|
|
167
|
+
--blue-850: oklch(40.21% 0.1604 265.7deg);
|
|
168
|
+
--blue-900: oklch(37.91% 0.1378 265.5deg);
|
|
169
|
+
--blue-950: oklch(28.23% 0.0874 267.9deg);
|
|
170
|
+
|
|
171
|
+
--radius: 8px;
|
|
172
|
+
--radius-medium: 6px;
|
|
173
|
+
--radius-small: 4px;
|
|
174
|
+
|
|
175
|
+
--icon-size: 16px;
|
|
176
|
+
--shadow: 0 0 40px var(--zinc-950);
|
|
177
|
+
|
|
178
|
+
font-family: var(--font-sans);
|
|
179
|
+
font-size: var(--font-size-small);
|
|
180
|
+
line-height: var(--line-height);
|
|
181
|
+
color: var(--zinc-100);
|
|
182
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The toolbar draws the notification badge of each app. These rules change only our badge:
|
|
2
|
+
// no border and a lighter color. Instead of the border, a mask cuts a gap in the shape of the
|
|
3
|
+
// badge out of our icon, so the real background (a gradient) shows around the badge.
|
|
4
|
+
// The selectors depend on the toolbar markup of Astro. They start with the same IDs as the
|
|
5
|
+
// rules of Astro, so they have a higher specificity and win without !important.
|
|
6
|
+
const APP = '#dev-bar #bar-container .item[data-app-id="mannisto-astro-metadata"]'
|
|
7
|
+
|
|
8
|
+
// The shapes of the badges of Astro.
|
|
9
|
+
// The shapes are in the coordinates of their own 10 px badge box.
|
|
10
|
+
const BADGE_SHAPES = {
|
|
11
|
+
warning:
|
|
12
|
+
'<path d="M7.29904 1.25c-.57735-1-2.02073-1-2.59808 0l-3.4641 6C.65951 8.25 1.3812 9.5 2.5359 9.5h6.9282c1.1547 0 1.8764-1.25 1.299-2.25l-3.46406-6Z"/>',
|
|
13
|
+
error: '<rect width="9" height="9" x=".5" y=".5" rx="4.5"/>',
|
|
14
|
+
info: '<rect width="9" height="9" x=".5" y=".5" rx="1.5"/>',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The position of the badge box, in px from the top and the right edge of the 20 px icon.
|
|
18
|
+
// Astro uses top -4 and right -6. Smaller values move the badge closer to the icon.
|
|
19
|
+
const BADGE_TOP = -4
|
|
20
|
+
const BADGE_RIGHT = -6
|
|
21
|
+
|
|
22
|
+
// The width of the gap around the badge, in px: the same as the line of the icon on screen.
|
|
23
|
+
// The icon (Phosphor, Bold weight) has a 24 unit line in a 256 unit grid,
|
|
24
|
+
// and the toolbar shows it at 20 px.
|
|
25
|
+
const BADGE_GAP = 24 * (20 / 256)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns a CSS mask image: the full icon, with a hole in the shape of a badge.
|
|
29
|
+
*/
|
|
30
|
+
function badgeMask(shape: string): string {
|
|
31
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
32
|
+
<mask id="m">
|
|
33
|
+
<rect width="20" height="20" fill="white"/>
|
|
34
|
+
<g transform="translate(${10 - BADGE_RIGHT} ${BADGE_TOP})" fill="black" stroke="black" stroke-width="${BADGE_GAP * 2}" stroke-linejoin="round">${shape}</g>
|
|
35
|
+
</mask>
|
|
36
|
+
<rect width="20" height="20" mask="url(#m)"/>
|
|
37
|
+
</svg>`
|
|
38
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The badge is in the shadow root of the Astro toolbar, where the design values of the
|
|
42
|
+
// panel do not reach. So its colors are the hex codes of the Tailwind values.
|
|
43
|
+
const BADGE_STYLE = `
|
|
44
|
+
${APP} .notification {
|
|
45
|
+
top: ${BADGE_TOP}px;
|
|
46
|
+
right: ${BADGE_RIGHT}px;
|
|
47
|
+
}
|
|
48
|
+
${APP} .notification svg :is(path, rect) {
|
|
49
|
+
stroke: none;
|
|
50
|
+
}
|
|
51
|
+
${APP} .notification[data-level="warning"] path {
|
|
52
|
+
fill: #facc15; /* yellow-400 */
|
|
53
|
+
}
|
|
54
|
+
${APP} .notification[data-level="error"] rect {
|
|
55
|
+
fill: #ef4444; /* red-500 */
|
|
56
|
+
}
|
|
57
|
+
${Object.entries(BADGE_SHAPES)
|
|
58
|
+
.map(([level, shape]) => {
|
|
59
|
+
return `
|
|
60
|
+
#dev-toolbar-root:not([data-no-notification]) ${APP} .icon:has(.notification[data-active][data-level="${level}"]) > svg {
|
|
61
|
+
mask: ${badgeMask(shape)} center / 100% 100% no-repeat;
|
|
62
|
+
}`
|
|
63
|
+
})
|
|
64
|
+
.join("")}
|
|
65
|
+
`
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Adds the badge style to the shadow root of the Astro toolbar, one time.
|
|
69
|
+
*/
|
|
70
|
+
export function styleBadge(): void {
|
|
71
|
+
const root = document.querySelector("astro-dev-toolbar")?.shadowRoot
|
|
72
|
+
if (!root || root.getElementById("mannisto-astro-metadata-badge")) return
|
|
73
|
+
|
|
74
|
+
const style = document.createElement("style")
|
|
75
|
+
style.id = "mannisto-astro-metadata-badge"
|
|
76
|
+
style.textContent = BADGE_STYLE
|
|
77
|
+
root.append(style)
|
|
78
|
+
}
|