feat!: rewrite site selectors (#176)

This commit is contained in:
uncenter 2025-02-26 11:29:44 -05:00 committed by GitHub
parent 39e75715a2
commit 55434c068d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 401 additions and 218 deletions

View File

@ -1,95 +1,5 @@
const GITHUB_SELECTORS = {
row: [
'.js-navigation-container[role=grid] > .js-navigation-item',
// Old commit details and pull request file tree.
'file-tree .ActionList-content',
'a.tree-browser-result',
// For the inner repository file sidepanel. Extra specificity to avoid matching icons on the new commit details page, which uses the same component.
'ul[aria-label="Files"] .PRIVATE_TreeView-item-content',
'.react-directory-filename-column',
'[aria-label="Parent directory"]',
],
filename: [
'div[role="rowheader"] > span',
'.ActionList-item-label',
'a.tree-browser-result > marked-text',
'.PRIVATE_TreeView-item-content > .PRIVATE_TreeView-item-content-text',
'.react-directory-filename-column .react-directory-filename-cell a',
'[aria-label="Parent directory"] div',
],
icon: [
'.octicon-file',
'.octicon-file-directory-fill',
'.octicon-file-directory-open-fill',
'.octicon-file-submodule',
'.react-directory-filename-column > svg',
'[aria-label="Parent directory"] svg',
'.PRIVATE_TreeView-item-visual > svg',
],
};
const GITLAB_SELECTORS = {
row: [
'.tree-table .tree-item',
'.file-header-content',
'.diff-tree-list .file-row',
],
filename: [
'.tree-item-file-name .tree-item-link span:last-of-type',
'.file-title-name',
'span.gl-truncate-component',
],
icon: [
'.folder-icon',
'.file-icon',
'span svg:has(use[href^="/assets/file_icons/"])',
],
};
const FORGEJO_SELECTORS = {
row: [
'#repo-files-table .entry',
'#diff-file-tree .item-file',
'#diff-file-tree .item-directory',
],
filename: ['.name a.muted', 'span.gt-ellipsis'],
icon: ['.octicon-file-directory-fill', '.octicon-file'],
};
const GITEA_SELECTORS = {
row: [
'#repo-files-table .repo-file-item',
'#diff-file-tree .item-file',
'#diff-file-tree .item-directory',
],
filename: ['.name a.muted', 'span.gt-ellipsis'],
icon: ['.octicon-file-directory-fill', '.octicon-file'],
};
function mergeSelectors(key: keyof typeof GITHUB_SELECTORS): string {
return [
...GITHUB_SELECTORS[key],
...GITLAB_SELECTORS[key],
...FORGEJO_SELECTORS[key],
...GITEA_SELECTORS[key],
].join(',');
}
export const SELECTORS = {
row: mergeSelectors('row'),
filename: mergeSelectors('filename'),
icon: mergeSelectors('icon'),
};
export const icons = {
x: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>',
};
export const ATTRIBUTE_PREFIX = 'data-catppuccin-file-explorer-icons';
export const MATCHES = [
'*://github.com/*',
'*://gitlab.com/*',
'*://codeberg.org/*',
'*://gitea.com/*',
];

View File

@ -2,24 +2,47 @@ import { defineContentScript } from 'wxt/sandbox';
import { observe } from 'selector-observer';
import { MATCHES, SELECTORS } from '@/constants';
import { type ReplacementSelectorSet, matches, sites } from '@/sites';
import { flavor } from '@/storage';
import { createStylesElement } from '@/utils';
import { injectStyles, replaceIconInRow } from './lib';
export default defineContentScript({
// Make sure `matches` URLs are updated in wxt.config.ts as well.
matches: MATCHES,
matches: matches,
runAt: 'document_start',
main() {
// Monitor DOM elements that match a CSS selector.
observe(SELECTORS.row, {
async add(row) {
await replaceIconInRow(row as HTMLElement);
},
});
const stylesEl = createStylesElement();
flavor.watch(injectStyles);
injectStyles();
for (const site of sites) {
if (site.domains.includes(window.location.hostname)) {
runReplacements(site.replacements, stylesEl);
// Assume URLs only have one matching site implementation. Can change this in the future.
return;
}
}
/* No matching domain. */
const replacements = sites.flatMap((site) => site.replacements);
runReplacements(replacements, stylesEl);
},
});
function runReplacements(
replacements: Array<ReplacementSelectorSet>,
stylesEl: Element,
) {
// Monitor DOM elements that match a CSS selector.
for (const replacement of replacements) {
observe(replacement.row, {
async add(rowEl: HTMLElement) {
await replaceIconInRow(rowEl, replacement);
},
});
}
const rawStyles = replacements.map(({ styles }) => styles || '').join('\n');
flavor.watch(() => injectStyles(stylesEl, rawStyles));
injectStyles(stylesEl, rawStyles);
}

View File

@ -1,40 +1,29 @@
import type { IconName } from '@/types';
import { getAssociations } from '@/associations';
import { ATTRIBUTE_PREFIX, SELECTORS } from '@/constants';
import { flavor, monochrome, specificFolders } from '@/storage';
import { createStylesElement } from '@/utils';
import { flavors } from '@catppuccin/palette';
import { ATTRIBUTE_PREFIX } from '@/constants';
import icons from '@/icons.json';
import type { ReplacementSelectorSet } from '@/sites';
export async function injectStyles() {
const styles = createStylesElement();
styles.textContent = /* css */ `
:root {
${flavors[await flavor.getValue()].colorEntries
export async function injectStyles(stylesEl: Element, siteStyles: string) {
stylesEl.textContent =
/* css */ `
:root {
${flavors[await flavor.getValue()].colorEntries
.map(([name, { hex }]) => `--ctp-${name}: ${hex};`)
.join('\n ')}
}
ul[aria-label="Files"] .PRIVATE_TreeView-directory-icon svg {
display: none !important;
}
svg[${ATTRIBUTE_PREFIX}-iconname$='_open']:has(~ svg.octicon-file-directory-open-fill:not([data-catppuccin-file-explorer-icons])),
svg:not([${ATTRIBUTE_PREFIX}-iconname$='_open']):has(~ svg.octicon-file-directory-fill:not([data-catppuccin-file-explorer-icons])),
svg[${ATTRIBUTE_PREFIX}]:has(+ .octicon-file) {
display: inline-block !important;
}
`.trim();
}
`.trim() + siteStyles;
}
async function createIconElement(
iconName: IconName,
fileName: string,
originalIcon: HTMLElement,
originalIconEl: HTMLElement,
): Promise<SVGSVGElement> {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
if (await monochrome.getValue()) {
@ -49,11 +38,11 @@ async function createIconElement(
svg.setAttribute(`${ATTRIBUTE_PREFIX}-iconname`, iconName);
svg.setAttribute(`${ATTRIBUTE_PREFIX}-filename`, fileName);
for (const attribute of originalIcon.getAttributeNames()) {
for (const attribute of originalIconEl.getAttributeNames()) {
if (!attribute.startsWith(ATTRIBUTE_PREFIX)) {
svg.setAttribute(
attribute,
originalIcon.getAttribute(attribute) as string,
originalIconEl.getAttribute(attribute) as string,
);
}
}
@ -61,102 +50,11 @@ async function createIconElement(
return svg;
}
export async function replaceIconInRow(row: HTMLElement) {
const icon = row.querySelector(SELECTORS.icon) as HTMLElement;
if (icon && !icon?.hasAttribute(ATTRIBUTE_PREFIX))
await replaceIcon(icon, row);
}
export async function replaceIcon(icon: HTMLElement, row: HTMLElement) {
const fileNameEl = row.querySelector(SELECTORS.filename) as HTMLElement;
if (!fileNameEl) return;
const fileName = fileNameEl.textContent
?.split('/')
.at(0)
.trim()
/* Remove [Unicode LEFT-TO-RIGHT MARK](https://en.wikipedia.org/wiki/Left-to-right_mark) used on GitLab's merge request diff file tree. */
.replace(/\u200E/g, '');
const isDir =
(icon.getAttribute('aria-label') === 'Directory' ||
icon.getAttribute('class')?.includes('octicon-file-directory-') ||
icon.classList.contains('icon-directory') ||
icon.classList.contains('folder-icon')) &&
!fileNameEl.getAttribute('aria-label')?.includes('(Symlink to file)');
const isSubmodule =
icon.classList.contains('octicon-file-submodule') ||
fileNameEl.getAttribute('aria-label')?.includes('(Submodule)');
const isOpen =
isDir && icon.classList.contains('octicon-file-directory-open-fill');
const fileExtensions: Array<string> = [];
// Avoid doing an explosive combination of extensions for very long filenames
// (most file systems do not allow files > 255 length) with lots of `.` characters
// https://github.com/microsoft/vscode/issues/116199
if (fileName.length <= 255) {
for (let i = 0; i < fileName.length; i += 1) {
if (fileName[i] === '.')
fileExtensions.push(fileName.toLowerCase().slice(i + 1));
}
}
const iconName = await findIconMatch(
fileName,
fileExtensions,
isDir,
isSubmodule,
);
await replaceElementWithIcon(
icon,
isOpen ? (`${iconName}_open` as IconName) : iconName,
fileName,
);
}
export async function replaceElementWithIcon(
icon: HTMLElement,
iconName: IconName,
fileName: string,
) {
const replacement = await createIconElement(iconName, fileName, icon);
const prevEl = icon.previousElementSibling;
if (prevEl?.hasAttribute(ATTRIBUTE_PREFIX)) {
replacement.replaceWith(prevEl);
}
// If the icon to replace is an icon from this extension, replace it with the new icon.
else if (icon.hasAttribute(ATTRIBUTE_PREFIX)) {
icon.replaceWith(replacement);
}
// If neither of the above, prepend the new icon in front of the original icon.
// If we remove the icon, GitHub code view crashes when you navigate through the
// tree view. Instead, we just hide it via `style` attribute (not CSS class).
else {
icon.style.display = 'none';
icon.before(replacement);
}
if (
icon.parentElement.classList.contains('PRIVATE_TreeView-directory-icon')
) {
const companion = await createIconElement(
(iconName.includes('_open')
? iconName.replace('_open', '')
: `${iconName}_open`) as IconName,
fileName,
icon,
);
replacement.after(companion);
}
}
async function findIconMatch(
fileName: string,
fileExtensions: Array<string>,
isDir: boolean,
isSubmodule: boolean,
isDirectory: boolean,
): Promise<IconName> {
// Special parent directory folder icon.
if (fileName === '..') return '_folder';
@ -166,7 +64,7 @@ async function findIconMatch(
if (useSpecificFolders && isSubmodule) return 'folder_git';
if (isDir) {
if (isDirectory) {
if (useSpecificFolders) {
if (fileName in associations.folderNames)
return associations.folderNames[fileName];
@ -191,3 +89,74 @@ async function findIconMatch(
return '_file';
}
export async function replaceIconInRow(
rowEl: HTMLElement,
selectors: ReplacementSelectorSet,
) {
const iconEl = rowEl.querySelector(selectors.icon) as HTMLElement;
console.log({ iconEl });
// Icon already has extension prefix, not necessary to replace again.
if (!iconEl || iconEl?.hasAttribute(ATTRIBUTE_PREFIX)) return;
const fileNameEl = rowEl.querySelector(selectors.filename) as HTMLElement;
if (!fileNameEl) return;
const fileName = fileNameEl.textContent
?.split('/')
.at(0)
.trim()
/* Remove [Unicode LEFT-TO-RIGHT MARK](https://en.wikipedia.org/wiki/Left-to-right_mark) used on GitLab's merge request diff file tree. */
.replace(/\u200E/g, '');
const fileExtensions: Array<string> = [];
// Avoid doing an explosive combination of extensions for very long filenames
// (most file systems do not allow files > 255 length) with lots of `.` characters
// https://github.com/microsoft/vscode/issues/116199
if (fileName.length <= 255) {
for (let i = 0; i < fileName.length; i += 1) {
if (fileName[i] === '.')
fileExtensions.push(fileName.toLowerCase().slice(i + 1));
}
}
const isDirectory = selectors.isDirectory(rowEl, fileNameEl, iconEl);
const isSubmodule = selectors.isSubmodule(rowEl, fileNameEl, iconEl);
const isCollapsable = selectors.isCollapsable(rowEl, fileNameEl, iconEl);
console.log({ isCollapsable });
const iconName = await findIconMatch(
fileName,
fileExtensions,
isSubmodule,
isDirectory,
);
const replacementEl = await createIconElement(iconName, fileName, iconEl);
// Check if element sibling before current element was inserted by extension, replace the old replacement with the new one instead.
const prevEl = iconEl.previousElementSibling;
if (prevEl?.hasAttribute(ATTRIBUTE_PREFIX)) {
replacementEl.replaceWith(prevEl);
}
// If the current icon element is an icon from this extension, replace it with the new icon.
else if (iconEl.hasAttribute(ATTRIBUTE_PREFIX)) {
iconEl.replaceWith(replacementEl);
}
// If neither of the above, prepend the new icon to the original icon element.
// If we remove the icon, GitHub code view crashes when you navigate through the
// tree view. Instead, we just hide it via `style` attribute (not CSS class).
else {
iconEl.style.display = 'none';
iconEl.before(replacementEl);
}
if (isCollapsable) {
const companionEl = await createIconElement(
`${iconName}_open` as IconName,
fileName,
iconEl,
);
replacementEl.after(companionEl);
}
}

43
src/sites/forgejo.ts Normal file
View File

@ -0,0 +1,43 @@
import type { ReplacementSelectorSet, Site } from '.';
import { ATTRIBUTE_PREFIX } from '../constants';
const mainRepositoryImplementation: ReplacementSelectorSet = {
row: '#repo-files-table .entry',
filename: '.name a',
icon: '.svg',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-directory-fill'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-submodule'),
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
/* Both commits and pull requests. */
const diffTreeImplementation: ReplacementSelectorSet = {
row: '.diff-file-tree-items .item-directory, .diff-file-tree-items .item-file',
filename: '.gt-ellipsis',
icon: '.octicon-file-directory-fill, .octicon-file',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-directory-fill'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-submodule'),
isCollapsable: (rowEl, fileNameEl, iconEl) =>
diffTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
};
diffTreeImplementation.styles = /* css */ `
${diffTreeImplementation.row} {
svg.octicon-file-directory-fill {
display: none !important;
}
svg.octicon-chevron-down ~ svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
svg.octicon-chevron-right ~ svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
display: inline-block !important;
}
}
`.trim();
export const forgejo: Site = {
domains: ['codeberg.org'],
replacements: [mainRepositoryImplementation, diffTreeImplementation],
};

43
src/sites/gitea.ts Normal file
View File

@ -0,0 +1,43 @@
import type { ReplacementSelectorSet, Site } from '.';
import { ATTRIBUTE_PREFIX } from '../constants';
const mainRepositoryImplementation: ReplacementSelectorSet = {
row: '#repo-files-table .repo-file-item',
filename: '.name a',
icon: '.svg',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-directory-fill'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-submodule'),
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
const diffTreeImplementation: ReplacementSelectorSet = {
row: '.diff-file-tree-items .item-directory, .diff-file-tree-items .item-file',
filename: '.gt-ellipsis',
icon: '.octicon-file-directory-fill, .octicon-file-directory-open-fill, .octicon-file',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-directory-fill') ||
iconEl.classList.contains('octicon-file-directory-open-fill'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-submodule'),
isCollapsable: (rowEl, fileNameEl, iconEl) =>
diffTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
};
diffTreeImplementation.styles = /* css */ `
${diffTreeImplementation.row} {
svg.octicon-file-directory-fill, svg.octicon-file-directory-open-fill {
display: none !important;
}
svg.octicon-chevron-down ~ svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
svg.octicon-chevron-right ~ svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
display: inline-block !important;
}
}
`.trim();
export const gitea: Site = {
domains: ['gitea.com'],
replacements: [mainRepositoryImplementation, diffTreeImplementation],
};

87
src/sites/github.ts Normal file
View File

@ -0,0 +1,87 @@
import type { ReplacementSelectorSet, Site } from '.';
import { ATTRIBUTE_PREFIX } from '../constants';
// For the inner repository file sidepanel. Extra specificity to avoid matching icons on the new commit details page, which uses the same component.
const repositorySideTreeImplementation: ReplacementSelectorSet = {
row: 'ul[aria-label="Files"] .PRIVATE_TreeView-item-content',
filename:
'.PRIVATE_TreeView-item-content > .PRIVATE_TreeView-item-content-text',
icon: '.PRIVATE_TreeView-item-visual svg',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.getAttribute('class')?.includes('octicon-file-directory-'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('octicon-file-submodule'),
isCollapsable: (rowEl, fileNameEl, iconEl) =>
repositorySideTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
};
repositorySideTreeImplementation.styles = /* css */ `
${repositorySideTreeImplementation.row} {
/* Hide directory icons by default. */
.PRIVATE_TreeView-directory-icon svg {
display: none !important;
}
/* Show relevant extension directory icon depending on open/closed state. */
svg[${ATTRIBUTE_PREFIX}-iconname$='_open']:has(~ svg.octicon-file-directory-open-fill:not([data-catppuccin-file-explorer-icons])),
svg:not([${ATTRIBUTE_PREFIX}-iconname$='_open']):has(~ svg.octicon-file-directory-fill:not([data-catppuccin-file-explorer-icons])),
svg[${ATTRIBUTE_PREFIX}]:has(+ .octicon-file) {
display: inline-block !important;
}
}
`.trim();
const repositoryMainImplementation: ReplacementSelectorSet = {
row: '.react-directory-filename-column',
filename: '.react-directory-filename-cell a',
icon: 'svg',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('icon-directory'),
isSubmodule: (_rowEl, fileNameEl, _iconEl) =>
fileNameEl.getAttribute('aria-label')?.includes('(Submodule)'),
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
const repositoryMainParentDirectoryImplementation: ReplacementSelectorSet = {
row: '#folder-row-0 [aria-label="Parent directory"]',
filename: 'div',
icon: 'svg',
isDirectory: (_rowEl, _fileNameEl, _iconEl) => true,
isSubmodule: (_rowEl, _fileNameEl, _iconEl) => false,
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
const pullRequestTreeImplementation: ReplacementSelectorSet = {
row: 'file-tree .ActionList-content',
filename: '.ActionList-item-label',
icon: '.ActionList-item-visual svg',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.getAttribute('aria-label') === 'Directory',
isSubmodule: (_rowEl, fileNameEl, _iconEl) =>
fileNameEl.getAttribute('aria-label')?.includes('(Submodule)'),
isCollapsable: (rowEl, fileNameEl, iconEl) =>
pullRequestTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
};
pullRequestTreeImplementation.styles = /* css */ `
${pullRequestTreeImplementation.row} {
/* Hide directory icons by default. */
${pullRequestTreeImplementation.icon}[aria-label="Directory"] {
display: none;
}
/* Show relevant extension directory icon depending on open/closed state. */
&[aria-expanded="true"] svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
&[aria-expanded="false"] svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
display: inline-block !important;
}
}
`.trim();
export const github: Site = {
domains: ['github.com'],
replacements: [
repositorySideTreeImplementation,
repositoryMainImplementation,
pullRequestTreeImplementation,
repositoryMainParentDirectoryImplementation,
],
};

67
src/sites/gitlab.ts Normal file
View File

@ -0,0 +1,67 @@
import type { ReplacementSelectorSet, Site } from '.';
import { ATTRIBUTE_PREFIX } from '../constants';
const repositoryMainImplementation: ReplacementSelectorSet = {
row: '.tree-table .tree-item',
filename: '.tree-item-file-name .tree-item-link',
icon: 'span svg:has(use)',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('folder-icon'),
isSubmodule: (_rowEl, fileNameEl, _iconEl) =>
fileNameEl.classList.contains('is-submodule'),
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
const fileContentsHeaderImplementation: ReplacementSelectorSet = {
row: '.project-show-files .file-header-content',
filename: '.file-title-name',
icon: 'span svg:has(use)',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('folder-icon'),
isSubmodule: (_rowEl, _fileNameEl, _iconEl) => false,
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
const commitDiffFileHeaderImplementation: ReplacementSelectorSet = {
row: '.js-diffs-batch .file-header-content',
filename: '.file-title-name',
icon: 'svg:has(+ .file-title-name)',
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
iconEl.classList.contains('folder-icon'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.getAttribute('data-testid') === 'folder-git-icon',
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
};
const mergeRequestsTreeImplementation: ReplacementSelectorSet = {
row: '.diff-tree-list .file-row',
filename: '.file-row-name',
icon: '.file-row-icon svg',
isDirectory: (rowEl, _fileNameEl, _iconEl) =>
rowEl.classList.contains('folder'),
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
iconEl.firstElementChild.getAttribute('href').endsWith('#folder-git'),
isCollapsable: (rowEl, fileNameEl, iconEl) =>
mergeRequestsTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
};
mergeRequestsTreeImplementation.styles = /* css */ `
/* Hide directory icons by default. */
${mergeRequestsTreeImplementation.row}.folder ${mergeRequestsTreeImplementation.icon} {
display: none !important;
}
/* Show relevant extension directory icon depending on open/closed state. */
${mergeRequestsTreeImplementation.row}.is-open svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
${mergeRequestsTreeImplementation.row}:not(.is-open) svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
display: inline-block !important;
}
`.trim();
export const gitlab: Site = {
domains: ['gitlab.com'],
replacements: [
repositoryMainImplementation,
fileContentsHeaderImplementation,
commitDiffFileHeaderImplementation,
mergeRequestsTreeImplementation,
],
};

40
src/sites/index.ts Normal file
View File

@ -0,0 +1,40 @@
import { forgejo } from './forgejo';
import { gitea } from './gitea';
import { github } from './github';
import { gitlab } from './gitlab';
export type FnWithContext<T> = (
rowEl: HTMLElement,
fileNameEl: HTMLElement,
iconEl: HTMLElement,
) => T;
export type ReplacementSelectorSet = {
row: string;
filename: string;
icon: string;
isDirectory: FnWithContext<boolean>;
isSubmodule: FnWithContext<boolean>;
isCollapsable: FnWithContext<boolean>;
styles?: string;
};
export type Site = {
domains: Array<string>;
replacements: Array<ReplacementSelectorSet>;
};
// import type { Site } from '.';
//
// export const mySite: SupportedSite = {
// urls: ['*://mysite.com/*'],
// replacements: [someImplementationFoo, someImplementationBar],
// styles: /* css */ `
// /* ... */
// `.trim(),
// };
export const sites: Array<Site> = [github, gitlab, gitea, forgejo];
export const matches: Array<string> = sites
.flatMap((site) => site.domains)
.map((domain) => `*://${domain}/*`);

View File

@ -6,7 +6,7 @@ import { optimize } from 'svgo';
import jiti from 'jiti';
import { MATCHES } from './src/constants';
import { matches } from './src/sites';
export default defineConfig({
srcDir: 'src',
@ -77,7 +77,7 @@ export default defineConfig({
manifest.content_scripts ??= [];
manifest.content_scripts.push({
// Make sure `matches` URLs are updated in src/entries/content/index.ts as well.
matches: MATCHES,
matches: matches,
run_at: 'document_start',
js: ['content-scripts/content.js'],
});
@ -90,6 +90,7 @@ export default defineConfig({
'https://github.com/catppuccin/catppuccin',
'https://gitlab.com/gitlab-org/gitlab',
'https://codeberg.org/forgejo/forgejo',
'https://gitea.com/gitea/gitea-mirror',
'https://gitea.catppuccin.com/catppuccin/catppuccin',
],
},