mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-03 20:27:20 +02:00
feat(mdx-loader): preserve hash in image src; support GH themed images (#6323)
* feat(mdx-loader): preserve hash in image src; support GH themed images * more refactor
This commit is contained in:
parent
472a4c881a
commit
217b62682d
8 changed files with 190 additions and 154 deletions
|
@ -124,7 +124,14 @@ export default async function mdxLoader(
|
|||
remarkPlugins: [
|
||||
...(reqOptions.beforeDefaultRemarkPlugins || []),
|
||||
...DEFAULT_OPTIONS.remarkPlugins,
|
||||
[transformImage, {staticDirs: reqOptions.staticDirs, filePath}],
|
||||
[
|
||||
transformImage,
|
||||
{
|
||||
staticDirs: reqOptions.staticDirs,
|
||||
filePath,
|
||||
siteDir: reqOptions.siteDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
transformLinks,
|
||||
{
|
||||
|
|
|
@ -12,6 +12,16 @@
|
|||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## Heading
|
||||
|
||||
```md
|
||||
|
|
|
@ -18,14 +18,24 @@ exports[`transformImage plugin transform md images to <img /> 1`] = `
|
|||
|
||||
<img alt={\\"img\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default} />
|
||||
|
||||
<img alt={\\"img from second static folder\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static2/img2.png\\").default} />
|
||||
<img alt={\\"img from second static folder\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static2/img2.png\\").default} />
|
||||
|
||||
<img alt={\\"img from second static folder\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static2/img2.png\\").default} />
|
||||
|
||||
<img alt={\\"img\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default} title=\\"Title\\" /> <img alt={\\"img\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/img.png\\").default} />
|
||||
<img alt={\\"img\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default} title=\\"Title\\" /> <img alt={\\"img\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default} />
|
||||
|
||||
<img alt={\\"img with "quotes"\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default} title=\\"'Quoted' title\\" />
|
||||
|
||||
<img alt={\\"site alias\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default} />
|
||||
|
||||
<img alt={\\"img with hash\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default + '#light'} />
|
||||
<img alt={\\"img with hash\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png\\").default + '#dark'} />
|
||||
|
||||
<img alt={\\"img with query\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10\\").default} />
|
||||
<img alt={\\"img with query\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10&h=10\\").default} />
|
||||
|
||||
<img alt={\\"img with both\\"} src={require(\\"![CWD]/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=[CWD]/node_modules/file-loader/dist/cjs.js!./static/img.png?w=10&h=10\\").default + '#light'} />
|
||||
|
||||
## Heading
|
||||
|
||||
\`\`\`md
|
||||
|
|
|
@ -28,17 +28,12 @@ const processFixture = async (name, options) => {
|
|||
};
|
||||
|
||||
const staticDirs = [
|
||||
// avoid hardcoding absolute in the snapshot
|
||||
`./${path.relative(
|
||||
process.cwd(),
|
||||
path.join(__dirname, '__fixtures__/static'),
|
||||
)}`,
|
||||
`./${path.relative(
|
||||
process.cwd(),
|
||||
path.join(__dirname, '__fixtures__/static2'),
|
||||
)}`,
|
||||
];
|
||||
|
||||
const siteDir = path.join(__dirname, '__fixtures__');
|
||||
|
||||
describe('transformImage plugin', () => {
|
||||
test('fail if image does not exist', async () => {
|
||||
await expect(
|
||||
|
@ -57,7 +52,7 @@ describe('transformImage plugin', () => {
|
|||
});
|
||||
|
||||
test('transform md images to <img />', async () => {
|
||||
const result = await processFixture('img', {staticDirs});
|
||||
const result = await processFixture('img', {staticDirs, siteDir});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
toMessageRelativeFilePath,
|
||||
posixPath,
|
||||
escapePath,
|
||||
getFileLoaderUtils,
|
||||
} from '@docusaurus/utils';
|
||||
import visit from 'unist-util-visit';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import fs from 'fs-extra';
|
||||
import escapeHtml from 'escape-html';
|
||||
import {
|
||||
posixPath,
|
||||
escapePath,
|
||||
toMessageRelativeFilePath,
|
||||
getFileLoaderUtils,
|
||||
} from '@docusaurus/utils';
|
||||
import type {Plugin, Transformer} from 'unified';
|
||||
import type {Image, Literal} from 'mdast';
|
||||
|
||||
|
@ -26,27 +26,33 @@ const {
|
|||
interface PluginOptions {
|
||||
filePath: string;
|
||||
staticDirs: string[];
|
||||
siteDir: string;
|
||||
}
|
||||
|
||||
const createJSX = (node: Image, pathUrl: string) => {
|
||||
const jsxNode = node;
|
||||
(jsxNode as unknown as Literal).type = 'jsx';
|
||||
(jsxNode as unknown as Literal).value = `<img ${
|
||||
node.alt ? `alt={"${escapeHtml(node.alt)}"} ` : ''
|
||||
}${`src={require("${inlineMarkdownImageFileLoader}${escapePath(
|
||||
pathUrl,
|
||||
)}").default}`}${node.title ? ` title="${escapeHtml(node.title)}"` : ''} />`;
|
||||
function toImageRequireNode(node: Image, imagePath: string, filePath: string) {
|
||||
const jsxNode = node as Literal & Partial<Image>;
|
||||
let relativeImagePath = posixPath(
|
||||
path.relative(path.dirname(filePath), imagePath),
|
||||
);
|
||||
relativeImagePath = `./${relativeImagePath}`;
|
||||
|
||||
if (jsxNode.url) {
|
||||
delete (jsxNode as Partial<Image>).url;
|
||||
}
|
||||
if (jsxNode.alt) {
|
||||
delete jsxNode.alt;
|
||||
}
|
||||
if (jsxNode.title) {
|
||||
delete jsxNode.title;
|
||||
}
|
||||
};
|
||||
const parsedUrl = url.parse(node.url);
|
||||
const hash = parsedUrl.hash ?? '';
|
||||
const search = parsedUrl.search ?? '';
|
||||
|
||||
const alt = node.alt ? `alt={"${escapeHtml(node.alt)}"} ` : '';
|
||||
const src = `require("${inlineMarkdownImageFileLoader}${
|
||||
escapePath(relativeImagePath) + search
|
||||
}").default${hash ? ` + '${hash}'` : ''}`;
|
||||
const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
|
||||
|
||||
Object.keys(jsxNode).forEach(
|
||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
||||
);
|
||||
|
||||
(jsxNode as Literal).type = 'jsx';
|
||||
jsxNode.value = `<img ${alt}src={${src}}${title} />`;
|
||||
}
|
||||
|
||||
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
||||
const imageExists = await fs.pathExists(imagePath);
|
||||
|
@ -59,36 +65,53 @@ async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function findImage(possiblePaths: string[], sourceFilePath: string) {
|
||||
async function getImageAbsolutePath(
|
||||
imagePath: string,
|
||||
{siteDir, filePath, staticDirs}: PluginOptions,
|
||||
) {
|
||||
if (imagePath.startsWith('@site/')) {
|
||||
const imageFilePath = path.join(siteDir, imagePath.replace('@site/', ''));
|
||||
await ensureImageFileExist(imageFilePath, filePath);
|
||||
return imageFilePath;
|
||||
} else if (path.isAbsolute(imagePath)) {
|
||||
// absolute paths are expected to exist in the static folder
|
||||
const possiblePaths = staticDirs.map((dir) => path.join(dir, imagePath));
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const possiblePath of possiblePaths) {
|
||||
if (await fs.pathExists(possiblePath)) {
|
||||
return possiblePath;
|
||||
const imageFilePath = possiblePath;
|
||||
if (await fs.pathExists(imageFilePath)) {
|
||||
return imageFilePath;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Image ${possiblePaths
|
||||
.map((p) => toMessageRelativeFilePath(p))
|
||||
.join(' or ')} used in ${toMessageRelativeFilePath(
|
||||
sourceFilePath,
|
||||
filePath,
|
||||
)} not found.`,
|
||||
);
|
||||
}
|
||||
// We try to convert image urls without protocol to images with require calls
|
||||
// going through webpack ensures that image assets exist at build time
|
||||
else {
|
||||
// relative paths are resolved against the source file's folder
|
||||
const imageFilePath = path.join(path.dirname(filePath), imagePath);
|
||||
await ensureImageFileExist(imageFilePath, filePath);
|
||||
return imageFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
async function processImageNode(
|
||||
node: Image,
|
||||
{filePath, staticDirs}: PluginOptions,
|
||||
) {
|
||||
async function processImageNode(node: Image, options: PluginOptions) {
|
||||
if (!node.url) {
|
||||
throw new Error(
|
||||
`Markdown image URL is mandatory in "${toMessageRelativeFilePath(
|
||||
filePath,
|
||||
options.filePath,
|
||||
)}" file`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(node.url);
|
||||
if (parsedUrl.protocol) {
|
||||
if (parsedUrl.protocol || !parsedUrl.pathname) {
|
||||
// pathname:// is an escape hatch,
|
||||
// in case user does not want his images to be converted to require calls going through webpack loader
|
||||
// we don't have to document this for now,
|
||||
|
@ -96,24 +119,11 @@ async function processImageNode(
|
|||
if (parsedUrl.protocol === 'pathname:') {
|
||||
node.url = node.url.replace('pathname://', '');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// images without protocol
|
||||
else if (path.isAbsolute(node.url)) {
|
||||
// absolute paths are expected to exist in the static folder
|
||||
const possibleImagePaths = staticDirs.map((dir) =>
|
||||
path.join(dir, node.url),
|
||||
);
|
||||
const imagePath = await findImage(possibleImagePaths, filePath);
|
||||
createJSX(node, posixPath(imagePath));
|
||||
}
|
||||
// We try to convert image urls without protocol to images with require calls
|
||||
// going through webpack ensures that image assets exist at build time
|
||||
else {
|
||||
// relative paths are resolved against the source file's folder
|
||||
const expectedImagePath = path.join(path.dirname(filePath), node.url);
|
||||
await ensureImageFileExist(expectedImagePath, filePath);
|
||||
createJSX(node, node.url.startsWith('./') ? node.url : `./${node.url}`);
|
||||
}
|
||||
|
||||
const imagePath = await getImageAbsolutePath(parsedUrl.pathname, options);
|
||||
toImageRequireNode(node, imagePath, options.filePath);
|
||||
}
|
||||
|
||||
const plugin: Plugin<[PluginOptions]> = (options) => {
|
||||
|
|
|
@ -23,7 +23,6 @@ import type {Link, Literal} from 'mdast';
|
|||
const {
|
||||
loaders: {inlineMarkdownLinkFileLoader},
|
||||
} = getFileLoaderUtils();
|
||||
const hashRegex = /#.*$/;
|
||||
|
||||
interface PluginOptions {
|
||||
filePath: string;
|
||||
|
@ -31,103 +30,69 @@ interface PluginOptions {
|
|||
siteDir: string;
|
||||
}
|
||||
|
||||
async function ensureAssetFileExist(
|
||||
fileSystemAssetPath: string,
|
||||
sourceFilePath: string,
|
||||
) {
|
||||
const assetExists = await fs.pathExists(fileSystemAssetPath);
|
||||
// transform the link node to a jsx link with a require() call
|
||||
function toAssetRequireNode(node: Link, assetPath: string, filePath: string) {
|
||||
const jsxNode = node as Literal & Partial<Link>;
|
||||
let relativeAssetPath = posixPath(
|
||||
path.relative(path.dirname(filePath), assetPath),
|
||||
);
|
||||
// require("assets/file.pdf") means requiring from a package called assets
|
||||
relativeAssetPath = `./${relativeAssetPath}`;
|
||||
|
||||
const parsedUrl = url.parse(node.url);
|
||||
const hash = parsedUrl.hash ?? '';
|
||||
const search = parsedUrl.search ?? '';
|
||||
|
||||
const href = `require('${inlineMarkdownLinkFileLoader}${
|
||||
escapePath(relativeAssetPath) + search
|
||||
}').default${hash ? ` + '${hash}'` : ''}`;
|
||||
const children = stringifyContent(node);
|
||||
const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
|
||||
|
||||
Object.keys(jsxNode).forEach(
|
||||
(key) => delete jsxNode[key as keyof typeof jsxNode],
|
||||
);
|
||||
|
||||
(jsxNode as Literal).type = 'jsx';
|
||||
jsxNode.value = `<a target="_blank" href={${href}}${title}>${children}</a>`;
|
||||
}
|
||||
|
||||
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
|
||||
const assetExists = await fs.pathExists(assetPath);
|
||||
if (!assetExists) {
|
||||
throw new Error(
|
||||
`Asset ${toMessageRelativeFilePath(
|
||||
fileSystemAssetPath,
|
||||
assetPath,
|
||||
)} used in ${toMessageRelativeFilePath(sourceFilePath)} not found.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// transform the link node to a jsx link with a require() call
|
||||
function toAssetRequireNode({
|
||||
node,
|
||||
filePath,
|
||||
requireAssetPath,
|
||||
}: {
|
||||
node: Link;
|
||||
filePath: string;
|
||||
requireAssetPath: string;
|
||||
}) {
|
||||
let relativeRequireAssetPath = posixPath(
|
||||
path.relative(path.dirname(filePath), requireAssetPath),
|
||||
);
|
||||
const hash = hashRegex.test(node.url)
|
||||
? node.url.substring(node.url.indexOf('#'))
|
||||
: '';
|
||||
|
||||
// require("assets/file.pdf") means requiring from a package called assets
|
||||
relativeRequireAssetPath = relativeRequireAssetPath.startsWith('./')
|
||||
? relativeRequireAssetPath
|
||||
: `./${relativeRequireAssetPath}`;
|
||||
|
||||
const href = `require('${inlineMarkdownLinkFileLoader}${escapePath(
|
||||
relativeRequireAssetPath,
|
||||
)}').default${hash ? ` + '${hash}'` : ''}`;
|
||||
const children = stringifyContent(node);
|
||||
const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
|
||||
|
||||
(node as unknown as Literal).type = 'jsx';
|
||||
(
|
||||
node as unknown as Literal
|
||||
).value = `<a target="_blank" href={${href}}${title}>${children}</a>`;
|
||||
}
|
||||
|
||||
// If the link looks like an asset link, we'll link to the asset,
|
||||
// and use a require("assetUrl") (using webpack url-loader/file-loader)
|
||||
// instead of navigating to such link
|
||||
async function convertToAssetLinkIfNeeded(
|
||||
node: Link,
|
||||
{filePath, siteDir, staticDirs}: PluginOptions,
|
||||
async function getAssetAbsolutePath(
|
||||
assetPath: string,
|
||||
{siteDir, filePath, staticDirs}: PluginOptions,
|
||||
) {
|
||||
const assetPath = node.url.replace(hashRegex, '');
|
||||
|
||||
const hasSiteAlias = assetPath.startsWith('@site/');
|
||||
const hasAssetLikeExtension =
|
||||
path.extname(assetPath) && !assetPath.match(/#|\.md$|\.mdx$|\.html$/);
|
||||
|
||||
const looksLikeAssetLink = hasSiteAlias || hasAssetLikeExtension;
|
||||
|
||||
if (!looksLikeAssetLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
function toAssetLinkNode(requireAssetPath: string) {
|
||||
toAssetRequireNode({
|
||||
node,
|
||||
filePath,
|
||||
requireAssetPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (assetPath.startsWith('@site/')) {
|
||||
const fileSystemAssetPath = path.join(
|
||||
siteDir,
|
||||
assetPath.replace('@site/', ''),
|
||||
);
|
||||
await ensureAssetFileExist(fileSystemAssetPath, filePath);
|
||||
toAssetLinkNode(fileSystemAssetPath);
|
||||
const assetFilePath = path.join(siteDir, assetPath.replace('@site/', ''));
|
||||
// The @site alias is the only way to believe that the user wants an asset.
|
||||
// Everything else can just be a link URL
|
||||
await ensureAssetFileExist(assetFilePath, filePath);
|
||||
return assetFilePath;
|
||||
} else if (path.isAbsolute(assetPath)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const staticDir of staticDirs) {
|
||||
const fileSystemAssetPath = path.join(staticDir, assetPath);
|
||||
if (await fs.pathExists(fileSystemAssetPath)) {
|
||||
toAssetLinkNode(fileSystemAssetPath);
|
||||
return;
|
||||
const assetFilePath = path.join(staticDir, assetPath);
|
||||
if (await fs.pathExists(assetFilePath)) {
|
||||
return assetFilePath;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const fileSystemAssetPath = path.join(path.dirname(filePath), assetPath);
|
||||
if (await fs.pathExists(fileSystemAssetPath)) {
|
||||
toAssetLinkNode(fileSystemAssetPath);
|
||||
const assetFilePath = path.join(path.dirname(filePath), assetPath);
|
||||
if (await fs.pathExists(assetFilePath)) {
|
||||
return assetFilePath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function processLinkNode(node: Link, options: PluginOptions) {
|
||||
|
@ -144,11 +109,22 @@ async function processLinkNode(node: Link, options: PluginOptions) {
|
|||
}
|
||||
|
||||
const parsedUrl = url.parse(node.url);
|
||||
if (parsedUrl.protocol) {
|
||||
if (parsedUrl.protocol || !parsedUrl.pathname) {
|
||||
// Don't process pathname:// here, it's used by the <Link> component
|
||||
return;
|
||||
}
|
||||
const hasSiteAlias = parsedUrl.pathname.startsWith('@site/');
|
||||
const hasAssetLikeExtension =
|
||||
path.extname(parsedUrl.pathname) &&
|
||||
!parsedUrl.pathname.match(/\.(?:mdx?|html)(?:#|$)/);
|
||||
if (!hasSiteAlias && !hasAssetLikeExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
await convertToAssetLinkIfNeeded(node, options);
|
||||
const assetPath = await getAssetAbsolutePath(parsedUrl.pathname, options);
|
||||
if (assetPath) {
|
||||
toAssetRequireNode(node, assetPath, options.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const plugin: Plugin<[PluginOptions]> = (options) => {
|
||||
|
|
|
@ -160,6 +160,29 @@ import ThemedImage from '@theme/ThemedImage';
|
|||
</BrowserWindow>
|
||||
```
|
||||
|
||||
### GitHub-style themed images
|
||||
|
||||
GitHub uses its own [image theming approach](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) with path fragments, which you can easily implement yourself.
|
||||
|
||||
To toggle the visibility of an image using the path fragment (for GitHub, it's `#gh-dark-mode-only` and `#gh-light-mode-only`), add the following to your custom CSS (you can also use your own suffix if you don't want to be coupled to GitHub):
|
||||
|
||||
```css title="src/css/custom.css"
|
||||
html[data-theme='light'] img[src$='#gh-dark-mode-only'],
|
||||
html[data-theme='dark'] img[src$='#gh-light-mode-only'] {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
<BrowserWindow>
|
||||
|
||||

|
||||
|
||||
</BrowserWindow>
|
||||
|
||||
## Static assets {#static-assets}
|
||||
|
||||
If a Markdown link or image has an absolute path, the path will be seen as a file path and will be resolved from the static directories. For example, if you have configured [static directories](../../static-assets.md) to be `['public', 'static']`, then for the following image:
|
||||
|
|
|
@ -172,6 +172,11 @@ div[class^='announcementBar_'] {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
html[data-theme='light'] img[src$='#gh-dark-mode-only'],
|
||||
html[data-theme='dark'] img[src$='#gh-light-mode-only'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Used to test CSS insertion order */
|
||||
.test-marker-site-custom-css-unique-rule {
|
||||
content: "site-custom-css-unique-rule";
|
||||
|
|
Loading…
Add table
Reference in a new issue