help-beta: Add prettier plugin for astro files.

Even though we have separate packages for `help-beta`, we have opted to
put the prettier plugin and config for astro files in the main project
itself, so that linting needs to be configured only at one place.
This commit is contained in:
Shubham Padia
2025-06-30 07:28:40 +00:00
committed by Tim Abbott
parent b630c1d175
commit f37ffd8937
9 changed files with 274 additions and 201 deletions

View File

@@ -1,8 +1,9 @@
---
import EmojiCodes from '../../../static/generated/emoji/emoji_codes.json';
import EmojiCodes from "../../../static/generated/emoji/emoji_codes.json";
const nameToCodePoint: {[key: string]: string} = EmojiCodes["name_to_codepoint"];
const rowHTML = (emoticon: string, codepoint: string, name: string) => (`
const nameToCodePoint: {[key: string]: string} =
EmojiCodes["name_to_codepoint"];
const rowHTML = (emoticon: string, codepoint: string, name: string) => `
<tr>
<td><code>${emoticon}</code></td>
<td>
@@ -12,14 +13,15 @@ const rowHTML = (emoticon: string, codepoint: string, name: string) => (`
class="emoji-big">
</td>
</tr>
`);
`;
let body = "";
const emoticonConversions: {[key: string]: string} = EmojiCodes["emoticon_conversions"]
const emoticonConversions: {[key: string]: string} =
EmojiCodes["emoticon_conversions"];
Object.keys(emoticonConversions).forEach((name: string) => {
const emoticon: string = emoticonConversions[name]!;
body += rowHTML(name, nameToCodePoint[emoticon.slice(1, -1)]!, emoticon)
})
body += rowHTML(name, nameToCodePoint[emoticon.slice(1, -1)]!, emoticon);
});
---
<table>

View File

@@ -1,27 +1,32 @@
---
import assert from "node:assert/strict";
import { fromHtml } from "hast-util-from-html";
import { toHtml } from "hast-util-to-html";
import {fromHtml} from "hast-util-from-html";
import {toHtml} from "hast-util-to-html";
const tree = fromHtml(await Astro.slots.render("default"), { fragment: true });
const tree = fromHtml(await Astro.slots.render("default"), {fragment: true});
const tree_with_removed_newlines = {
type: "root",
children: tree.children.filter((child) => {
if (child.type === "text" && child.value === "\n") {
return false;
}
return true;
}),
}
type: "root",
children: tree.children.filter((child) => {
if (child.type === "text" && child.value === "\n") {
return false;
}
return true;
}),
};
const first_element = tree_with_removed_newlines.children[0];
assert(first_element?.type === "element" && ["ol", "ul"].includes(first_element.tagName));
assert(
first_element?.type === "element" &&
["ol", "ul"].includes(first_element.tagName),
);
const flattened = {
...first_element,
children: tree_with_removed_newlines.children.flatMap((other) => {
assert(other.type === "element" && other.tagName === first_element.tagName);
return other.children;
}),
...first_element,
children: tree_with_removed_newlines.children.flatMap((other) => {
assert(
other.type === "element" && other.tagName === first_element.tagName,
);
return other.children;
}),
};
---

View File

@@ -1,9 +1,9 @@
---
import ZulipIconsKeyboard from "~icons/zulip-icon/keyboard"
import ZulipIconsKeyboard from "~icons/zulip-icon/keyboard";
let { title } = Astro.props;
let {title} = Astro.props;
if (!title) {
title = "Keyboard Shortcut";
title = "Keyboard Shortcut";
}
---
@@ -16,10 +16,13 @@ component along with a custom icon (keyboard in out case).
Link: https://github.com/withastro/starlight/pull/2261
-->
<aside aria-label={title} class={`starlight-aside starlight-aside--tip`}>
<p class="starlight-aside__title" aria-hidden="true">
<ZulipIconsKeyboard fill="currentColor" class="starlight-aside__icon" />{title}
</p>
<div class="starlight-aside__content">
<slot />
</div>
<p class="starlight-aside__title" aria-hidden="true">
<ZulipIconsKeyboard
fill="currentColor"
class="starlight-aside__icon"
/>{title}
</p>
<div class="starlight-aside__content">
<slot />
</div>
</aside>

View File

@@ -1,5 +1,5 @@
---
import { SHOW_RELATIVE_LINKS, SHOW_BILLING_HELP_LINKS } from "astro:env/client";
import {SHOW_RELATIVE_LINKS, SHOW_BILLING_HELP_LINKS} from "astro:env/client";
import assert from "node:assert";
import RawZulipIconGear from "~icons/zulip-icon/gear?raw";
import RawZulipIconHash from "~icons/zulip-icon/hash?raw";
@@ -17,147 +17,147 @@ import RawZulipIconInfo from "~icons/zulip-icon/info?raw";
import RawZulipIconHelp from "~icons/zulip-icon/help?raw";
const PERSONAL_SETTINGS_TYPE = "Personal settings";
const ORGANIZATION_SETTINGS_TYPE = "Organization settings"
const ORGANIZATION_SETTINGS_TYPE = "Organization settings";
// This list has been transformed one-off from `help_settings_links.py`, we
// have added a comment in that file to update this list in case of any
// changes.
const setting_link_mapping: {
[key: string]: {
setting_type: string,
setting_name: string,
setting_link: string,
}
setting_type: string;
setting_name: string;
setting_link: string;
};
} = {
// a mapping from the setting identifier that is the same as the final URL
// breadcrumb to that setting to the name of its setting type, the setting
// name as it appears in the user interface, and a relative link that can
// be used to get to that setting
"profile": {
profile: {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Profile",
setting_link: "/#settings/profile"
setting_link: "/#settings/profile",
},
"account-and-privacy": {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Account & privacy",
setting_link: "/#settings/account-and-privacy"
setting_link: "/#settings/account-and-privacy",
},
"preferences": {
preferences: {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Preferences",
setting_link: "/#settings/preferences"
setting_link: "/#settings/preferences",
},
"notifications": {
notifications: {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Notifications",
setting_link: "/#settings/notifications"
setting_link: "/#settings/notifications",
},
"your-bots": {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Bots",
setting_link: "/#settings/your-bots"
setting_link: "/#settings/your-bots",
},
"alert-words": {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Alert words",
setting_link: "/#settings/alert-words"
setting_link: "/#settings/alert-words",
},
"uploaded-files": {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Uploaded files",
setting_link: "/#settings/uploaded-files"
setting_link: "/#settings/uploaded-files",
},
"topics": {
topics: {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Topics",
setting_link: "/#settings/topics"
setting_link: "/#settings/topics",
},
"muted-users": {
setting_type: PERSONAL_SETTINGS_TYPE,
setting_name: "Muted users",
setting_link: "/#settings/muted-users"
setting_link: "/#settings/muted-users",
},
"organization-profile": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Organization profile",
setting_link: "/#organization/organization-profile"
setting_link: "/#organization/organization-profile",
},
"organization-settings": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Organization settings",
setting_link: "/#organization/organization-settings"
setting_link: "/#organization/organization-settings",
},
"organization-permissions": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Organization permissions",
setting_link: "/#organization/organization-permissions"
setting_link: "/#organization/organization-permissions",
},
"default-user-settings": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Default user settings",
setting_link: "/#organization/organization-level-user-defaults"
setting_link: "/#organization/organization-level-user-defaults",
},
"emoji-settings": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Custom emoji",
setting_link: "/#organization/emoji-settings"
setting_link: "/#organization/emoji-settings",
},
"auth-methods": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Authentication methods",
setting_link: "/#organization/auth-methods"
setting_link: "/#organization/auth-methods",
},
"users": {
users: {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Users",
setting_link: "/#organization/users/active"
setting_link: "/#organization/users/active",
},
"deactivated": {
deactivated: {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Users",
setting_link: "/#organization/users/deactivated"
setting_link: "/#organization/users/deactivated",
},
"invitations": {
invitations: {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Users",
setting_link: "/#organization/users/invitations"
setting_link: "/#organization/users/invitations",
},
"bot-list-admin": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Bots",
setting_link: "/#organization/bot-list-admin"
setting_link: "/#organization/bot-list-admin",
},
"default-channels-list": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Default channels",
setting_link: "/#organization/default-channels-list"
setting_link: "/#organization/default-channels-list",
},
"linkifier-settings": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Linkifiers",
setting_link: "/#organization/linkifier-settings"
setting_link: "/#organization/linkifier-settings",
},
"playground-settings": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Code playgrounds",
setting_link: "/#organization/playground-settings"
setting_link: "/#organization/playground-settings",
},
"profile-field-settings": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Custom profile fields",
setting_link: "/#organization/profile-field-settings"
setting_link: "/#organization/profile-field-settings",
},
"data-exports-admin": {
setting_type: ORGANIZATION_SETTINGS_TYPE,
setting_name: "Data exports",
setting_link: "/#organization/data-exports-admin"
}
setting_link: "/#organization/data-exports-admin",
},
};
type RelativeLinkInfo = {
label: string;
relative_link: string;
label: string;
relative_link: string;
};
const default_template_for_relative_links = `
@@ -165,130 +165,129 @@ const default_template_for_relative_links = `
<li>Click on the <strong>gear</strong> (${RawZulipIconGear}) icon in the upper right corner of the web or desktop app.</li>
<li>Select {item}.</li>
</ol>
`
`;
const relative_link_mapping: Record<
string,
{
data: Record<string, RelativeLinkInfo>;
template: string;
is_link_relative: () => boolean;
}
string,
{
data: Record<string, RelativeLinkInfo>;
template: string;
is_link_relative: () => boolean;
}
> = {
gear: {
data: {
"channel-settings": {
label: `${RawZulipIconHash} Channel settings`,
relative_link: "/#channels/subscribed",
},
settings: {
label: `${RawZulipIconTool} Personal Settings`,
relative_link: "/#settings/profile",
},
"organization-settings": {
label: `${RawZulipIconBuilding} Organization settings`,
relative_link: "/#organization/organization-profile",
},
"group-settings": {
label: `${RawZulipIconUserGroupCog} Group settings`,
relative_link: "/#groups/your",
},
stats: {
label: `${RawZulipIconBarChart} Usage statistics`,
relative_link: "/stats",
},
integrations: {
label: `${RawZulipIconGitPullRequest} Integrations`,
relative_link: "/integrations/",
},
"about-zulip": {
label: "About Zulip",
relative_link: "/#about-zulip",
},
gear: {
data: {
"channel-settings": {
label: `${RawZulipIconHash} Channel settings`,
relative_link: "/#channels/subscribed",
},
settings: {
label: `${RawZulipIconTool} Personal Settings`,
relative_link: "/#settings/profile",
},
"organization-settings": {
label: `${RawZulipIconBuilding} Organization settings`,
relative_link: "/#organization/organization-profile",
},
"group-settings": {
label: `${RawZulipIconUserGroupCog} Group settings`,
relative_link: "/#groups/your",
},
stats: {
label: `${RawZulipIconBarChart} Usage statistics`,
relative_link: "/stats",
},
integrations: {
label: `${RawZulipIconGitPullRequest} Integrations`,
relative_link: "/integrations/",
},
"about-zulip": {
label: "About Zulip",
relative_link: "/#about-zulip",
},
},
template: default_template_for_relative_links,
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
template: default_template_for_relative_links,
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
"gear-billing": {
data: {
plans: {
label: `${RawZulipIconRocket} Plans and pricing`,
relative_link: "/plans/",
},
billing: {
label: `${RawZulipIconCreditCard} Billing`,
relative_link: "/billing/",
},
"gear-billing": {
data: {
plans: {
label: `${RawZulipIconRocket} Plans and pricing`,
relative_link: "/plans/",
},
billing: {
label: `${RawZulipIconCreditCard} Billing`,
relative_link: "/billing/",
},
},
template: default_template_for_relative_links,
is_link_relative: () => SHOW_RELATIVE_LINKS && SHOW_BILLING_HELP_LINKS,
},
template: default_template_for_relative_links,
is_link_relative: () => SHOW_RELATIVE_LINKS && SHOW_BILLING_HELP_LINKS,
},
help: {
data: {
"keyboard-shortcuts": {
label: `${RawZulipIconKeyboard} Keyboard shortcuts`,
relative_link: "/#keyboard-shortcuts",
},
"message-formatting": {
label: `${RawZulipIconEdit} Message formatting`,
relative_link: "/#message-formatting",
},
"search-filters": {
label: `${RawZulipIconManageSearch} Search filters`,
relative_link: "/#search-operators",
},
"about-zulip": {
label: `${RawZulipIconInfo} About Zulip`,
relative_link: "/#about-zulip",
},
},
template: `
help: {
data: {
"keyboard-shortcuts": {
label: `${RawZulipIconKeyboard} Keyboard shortcuts`,
relative_link: "/#keyboard-shortcuts",
},
"message-formatting": {
label: `${RawZulipIconEdit} Message formatting`,
relative_link: "/#message-formatting",
},
"search-filters": {
label: `${RawZulipIconManageSearch} Search filters`,
relative_link: "/#search-operators",
},
"about-zulip": {
label: `${RawZulipIconInfo} About Zulip`,
relative_link: "/#about-zulip",
},
},
template: `
<ol>
<li>Click on the <strong>Help menu</strong> (${RawZulipIconHelp}) icon in the upper right corner of the app.</li>
<li>Select {item}.</li>
</ol>
`,
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
channel: {
data: {
all: {
label: "All",
relative_link: "/#channels/all",
},
"not-subscribed": {
label: "Not subscribed",
relative_link: "/#channels/notsubscribed",
},
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
template: `
channel: {
data: {
all: {
label: "All",
relative_link: "/#channels/all",
},
"not-subscribed": {
label: "Not subscribed",
relative_link: "/#channels/notsubscribed",
},
},
template: `
<ol>
<li>Click on the <strong>gear</strong> (${RawZulipIconGear}) icon in the upper right corner of the web or desktop app.</li>
<li>Select ${RawZulipIconHash} <strong>Channel settings</strong>.</li>
<li>Click {item} in the upper left.</li>
</ol>
`,
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
group: {
data: {
all: {
label: "All groups",
relative_link: "/#groups/all",
},
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
template: `
group: {
data: {
all: {
label: "All groups",
relative_link: "/#groups/all",
},
},
template: `
<ol>
<li>Click on the <strong>gear</strong> (${RawZulipIconGear}) icon in the upper right corner of the web or desktop app.</li>
<li>Select ${RawZulipIconUserGroupCog} <strong>Group settings</strong>.</li>
<li>Click {item} in the upper left.</li>
</ol>
`,
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
is_link_relative: () => SHOW_RELATIVE_LINKS,
},
};
const getSettingsMarkdown = (setting_type: string, setting_name: string) => `
<ol>
<li>
@@ -302,22 +301,19 @@ const getSettingsMarkdown = (setting_type: string, setting_name: string) => `
On the left, click <b>${setting_name}</b>.
</li>
</ol>
`
`;
const getSettingsHTML = (
setting_key: string,
SHOW_RELATIVE_LINKS: boolean
setting_key: string,
SHOW_RELATIVE_LINKS: boolean,
): string => {
const {
setting_type,
setting_name,
setting_link,
} = setting_link_mapping[setting_key]!;
const {setting_type, setting_name, setting_link} =
setting_link_mapping[setting_key]!;
if (!SHOW_RELATIVE_LINKS) {
return getSettingsMarkdown(setting_type, setting_name);
}
const relativeLink = `<a href="${setting_link}">${setting_name}</a>`;
// The "Bots" label appears in both Personal and
@@ -326,32 +322,43 @@ const getSettingsHTML = (
// As for the the case of "Users", it refers to the Users tab in
// organization settings. Since the users tab has multiple sub tabs
// like active, deactivated etc., we need a way to point to them.
const label = (setting_name === "Bots" || setting_name === "Users")
? `Navigate to the ${relativeLink} tab of the <b>${setting_type}</b> menu.`
: `Go to ${relativeLink}.`;
const label =
setting_name === "Bots" || setting_name === "Users"
? `Navigate to the ${relativeLink} tab of the <b>${setting_type}</b> menu.`
: `Go to ${relativeLink}.`;
return `<ol>
<li>${label}</li>
</ol>`;
}
};
const RELATIVE_NAVIGATION_HANDLERS_BY_TYPE: Record<string, (key: string) => string> = {};
const RELATIVE_NAVIGATION_HANDLERS_BY_TYPE: Record<
string,
(key: string) => string
> = {};
for (const type in relative_link_mapping) {
const { data, template, is_link_relative } = relative_link_mapping[type]!;
const {data, template, is_link_relative} = relative_link_mapping[type]!;
RELATIVE_NAVIGATION_HANDLERS_BY_TYPE[type] = (key: string) => {
const { label, relative_link } = data[key]!;
const formattedItem = is_link_relative() ? `<a href="${relative_link}">${label}</a>` : `<strong>${label}</strong>`;
return template.replace("{item}", formattedItem);
};
RELATIVE_NAVIGATION_HANDLERS_BY_TYPE[type] = (key: string) => {
const {label, relative_link} = data[key]!;
const formattedItem = is_link_relative()
? `<a href="${relative_link}">${label}</a>`
: `<strong>${label}</strong>`;
return template.replace("{item}", formattedItem);
};
}
const { identifier } = Astro.props;
const {identifier} = Astro.props;
const navigation_link_type = identifier.split("/")[0];
if (navigation_link_type !== "settings" && navigation_link_type !== "relative" ) {
throw new Error("Invalid navigation link type. Only `settings` or `relative` is allowed.");
if (
navigation_link_type !== "settings" &&
navigation_link_type !== "relative"
) {
throw new Error(
"Invalid navigation link type. Only `settings` or `relative` is allowed.",
);
}
let resultHTML: string | undefined;
@@ -363,6 +370,6 @@ if (navigation_link_type === "settings") {
resultHTML = RELATIVE_NAVIGATION_HANDLERS_BY_TYPE[link_type]!(key);
}
assert(resultHTML !== undefined);
---
<Fragment set:html={resultHTML} />