import browser from "webextension-polyfill"; import {createValue, type Value} from "@holllo/webextension-storage"; import {Data} from "./enums.js"; /** * Definition of a Markdown snippet to be used with the Markdown Toolbar feature. */ export type MarkdownSnippet = { /** Whether the snippet is enabled and should show in the toolbar. */ enabled: boolean; /** A unique identifier for the snippet. */ id: number; /** Whether the snippet should be placed in the "More..." dropdown. */ inDropdown: boolean; /** The Markdown content of the snippet. */ markdown: string; /** The name of the snippet to be shown in the toolbar. */ name: string; /** The position of the snippet in the toolbar. */ position: number; /** The keyboard shortcut for this snippet. */ shortcut?: string; }; /** * Markers used to specify where the cursor should be placed after the snippet * is inserted. */ export enum MarkdownSnippetMarker { /** * The marker for the default position the cursor should be placed at. */ Cursor = "", /** * If text was selected before the snippet was inserted, the cursor will first * attempt to be placed at the `SelectedCursor` position, if there is no * `SelectedCursor` in the snippet then `Cursor` is used instead. The selected * text will be inserted at the `Cursor` position. */ SelectedCursor = "", } /* eslint-disable-next-line @typescript-eslint/naming-convention */ const {Cursor, SelectedCursor} = MarkdownSnippetMarker; export const builtinSnippets: MarkdownSnippet[] = [ { enabled: true, inDropdown: false, markdown: `[${Cursor}](${SelectedCursor})`, name: "Link", }, { enabled: true, inDropdown: false, markdown: `\`\`\`${SelectedCursor}\n${Cursor}\n\`\`\``, name: "Code", }, { enabled: true, inDropdown: false, markdown: `~~${Cursor}~~`, name: "Strikethrough", }, { enabled: true, inDropdown: false, markdown: `
\nClick to expand spoiler.\n\n${Cursor}\n
`, name: "Spoilerbox", }, { enabled: true, inDropdown: true, markdown: `**${Cursor}**`, name: "Bold", }, { enabled: true, inDropdown: true, markdown: `\n\n---\n\n${Cursor}`, name: "Horizontal Divider", }, { enabled: true, inDropdown: true, markdown: `\`${Cursor}\``, name: "Inline Code", }, { enabled: true, inDropdown: true, markdown: `*${Cursor}*`, name: "Italic", }, { enabled: true, inDropdown: true, markdown: `1. ${Cursor}`, name: "Ordered List", }, { enabled: true, inDropdown: true, markdown: `${Cursor}`, name: "Small", }, { enabled: true, inDropdown: true, markdown: `* ${Cursor}`, name: "Unordered List", }, ].map((snippet, index) => { return { ...snippet, // Make the builtin snippets use a negative ID so there will never be a // conflict with user-created snippets if for some reason they need to be // reinserted into storage. id: index - 100, position: index + 1, } satisfies MarkdownSnippet; }); /** * Create a {@link Value}-wrapped {@link MarkdownSnippet}. */ export async function createValueMarkdownSnippet( snippet: MarkdownSnippet, ): Promise> { return createValue({ deserialize: (input) => JSON.parse(input) as MarkdownSnippet, serialize: (input) => JSON.stringify(input), key: `${Data.MarkdownSnippet}-${snippet.id}`, value: snippet, storage: browser.storage.sync, }); } /** * Get all Markdown snippets from storage and combine them into a single array. */ export async function collectMarkdownSnippets(): Promise< Array> > { const storage = await browser.storage.sync.get(); const snippets: Array> = []; for (const [key, value] of Object.entries(storage)) { if (!key.startsWith(Data.MarkdownSnippet)) { continue; } snippets.push( await createValueMarkdownSnippet( JSON.parse(value as string) as MarkdownSnippet, ), ); } if (snippets.length === 0) { // If no snippets are in storage, grab all the builtin ones and save them. snippets.push( ...(await Promise.all( builtinSnippets.map(async (snippet) => createValueMarkdownSnippet(snippet), ), )), ); for (const snippet of snippets) { await snippet.save(); } } return sortSnippets(snippets); } /** * Create a new Markdown snippet ID by getting the current highest existing ID * in storage and adding 1 to it. Defaults to 1 when no there are no existing * snippets. */ export async function newMarkdownSnippetId(): Promise { const snippets = await collectMarkdownSnippets(); let newId = 1; if (snippets.length > 0) { newId = snippets.sort((a, b) => b.value.id - a.value.id)[0].value.id + 1; } // Builtin snippets will have a negative ID so reset back to 1 if only builtin // snippets are in storage. if (newId < 0) { newId = 1; } return newId; } /** * Sort the snippets by their position first and their name second. */ export function sortSnippets( snippets: Array>, ): Array> { return snippets.sort((a, b) => { const position = a.value.position - b.value.position; if (position === 0) { return a.value.name.localeCompare(b.value.name); } return position; }); } /** A processed {@linkcode MarkdownSnippet} shortcut string. */ export type ProcessedSnippetShortcut = { /** Whether the ALT key is required for this shortcut. */ alt: boolean; /** Whether the CTRL key is required for this shortcut. */ ctrl: boolean; /** Whether the SHIFT key is required for this shortcut. */ shift: boolean; /** The main key to trigger the shortcut. */ key: string; }; /** * Process a {@linkcode MarkdownSnippet} shortcut string. * * The expected structure of a shortcut string is modifier keys in any order * separated by a `+` and the main key on the end. For example `ALT+CTRL+B` is * valid but `ALT+B+CTRL` is not. The casing of the letters is not relevant. * * This function performs no validation on the string and simply returns any * input as-is. In the `ALT+B+CTRL` example, `CTRL` is what would be returned as * the main key. */ export function processSnippetShortcut( shortcut: string, ): ProcessedSnippetShortcut { const components = shortcut.toLowerCase().split("+"); return { alt: components.includes("alt"), ctrl: components.includes("ctrl"), shift: components.includes("shift"), key: components[components.length - 1].toLowerCase(), } satisfies ProcessedSnippetShortcut; }