[packages][m]: mv @flowershow/remark-wiki-link here
This commit is contained in:
parent
c61a2db628
commit
ee6812974d
4
packages/remark-callouts/.babelrc
Normal file
4
packages/remark-callouts/.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
18
packages/remark-callouts/.eslintrc.json
Normal file
18
packages/remark-callouts/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
4
packages/remark-callouts/.mocharc.yaml
Normal file
4
packages/remark-callouts/.mocharc.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
extension: [ts]
|
||||
node-option:
|
||||
- experimental-specifier-resolution=node
|
||||
- loader=ts-node/esm
|
||||
121
packages/remark-callouts/README.md
Normal file
121
packages/remark-callouts/README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# remark-callouts
|
||||
|
||||
Remark plugin to add support for blockquote-based callouts/admonitions similar to the approach of [Obsidian](https://help.obsidian.md/How+to/Use+callouts) and [Microsoft Learn](https://learn.microsoft.com/en-us/contribute/markdown-reference#alerts-note-tip-important-caution-warning) style.
|
||||
|
||||
Using this plugin, markdown like this:
|
||||
|
||||
```md
|
||||
> [!tip]
|
||||
> hello callout
|
||||
```
|
||||
|
||||
Would render as a callout like this:
|
||||
|
||||
<img width="645" alt="Tip callout block" src="https://user-images.githubusercontent.com/42637597/193016397-49a90b44-cf3d-4eeb-9ad6-c0c1e374ed27.png">
|
||||
|
||||
## Features supported
|
||||
|
||||
- [x] Supports blockquote style callouts
|
||||
- [x] Supports nested blockquote callouts
|
||||
- [x] Supports 13 types out of the box (with appropriate styling in default theme) - see list below
|
||||
- [x] Supports aliases for types
|
||||
- [x] Defaults to note callout for all other types eg. `> [!xyz]`
|
||||
- [x] Supports dark and light mode styles
|
||||
|
||||
Future support:
|
||||
|
||||
- [ ] Support custom types and icons
|
||||
- [ ] Support custom aliases
|
||||
- [ ] Support Foldable callouts
|
||||
- [ ] Support custom styles
|
||||
|
||||
## Geting Started
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install remark-callouts
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import callouts from "remark-callouts";
|
||||
|
||||
await remark()
|
||||
.use(remarkParse)
|
||||
.use(callouts)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeStringify).process(`\
|
||||
> [!tip]
|
||||
> hello callout
|
||||
`);
|
||||
```
|
||||
|
||||
HTML output
|
||||
|
||||
```js
|
||||
<div>
|
||||
<blockquote class="callout">
|
||||
<div class="callout-title tip">
|
||||
<span class="callout-icon">
|
||||
<svg>...</svg>
|
||||
</span>
|
||||
<strong>Tip</strong>
|
||||
</div>
|
||||
<div class="callout-content">
|
||||
<p>hello callout</p>
|
||||
</div>
|
||||
</blockquote>
|
||||
</div>
|
||||
```
|
||||
|
||||
Import the styles in your .css file
|
||||
|
||||
```css
|
||||
@import "remark-callouts/styles.css";
|
||||
```
|
||||
|
||||
or in your app.js
|
||||
|
||||
```js
|
||||
import "remark-callouts/styles.css";
|
||||
```
|
||||
|
||||
### Supported Callout Types
|
||||
|
||||
- note
|
||||
- tip `aliases: hint, important`
|
||||
- warning `alises: caution, attention`
|
||||
- abstract `aliases: summary, tldr`
|
||||
- info
|
||||
- todo
|
||||
- success `aliases: check, done`
|
||||
- question `aliases: help, faq`
|
||||
- failure `aliases: fail, missing`
|
||||
- danger `alias: error`
|
||||
- bug
|
||||
- example
|
||||
- quote `alias: cite`
|
||||
|
||||
# Change Log
|
||||
|
||||
## [2.0.0] - 2022-11-21
|
||||
|
||||
### Added
|
||||
|
||||
- Classname for icon.
|
||||
|
||||
### Changed
|
||||
|
||||
- Extract css styles which can be imported separately.
|
||||
|
||||
## [1.0.2] - 2022-11-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Case insensitive match for types.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
41
packages/remark-callouts/package.json
Normal file
41
packages/remark-callouts/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@flowershow/remark-callouts",
|
||||
"version": "1.0.0",
|
||||
"description": "remark plugin to add support for blockquote-based admonitions/callouts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/flowershow/flowershow.git",
|
||||
"directory": "packages/remark-callouts"
|
||||
},
|
||||
"keywords": [
|
||||
"remark",
|
||||
"remark-plugin",
|
||||
"markdown",
|
||||
"admonitions",
|
||||
"callouts",
|
||||
"obsidian"
|
||||
],
|
||||
"author": "Rufus Pollock",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/flowershow/flowershow/issues"
|
||||
},
|
||||
"homepage": "https://github.com/flowershow/flowershow#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^1.2.0",
|
||||
"svg-parser": "^2.0.4",
|
||||
"unist-util-visit": "^4.1.0"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./styles.css": "./styles.css"
|
||||
}
|
||||
}
|
||||
64
packages/remark-callouts/project.json
Normal file
64
packages/remark-callouts/project.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "remark-callouts",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/remark-callouts/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["packages/remark-callouts/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "TS_NODE_PROJECT='packages/remark-callouts/tsconfig.spec.json' mocha --config packages/remark-callouts/.mocharc.yaml packages/remark-callouts/test/remark-callouts.spec.ts"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"entryFile": "packages/remark-callouts/src/index.ts",
|
||||
"outputPath": "packages/remark-callouts/dist",
|
||||
"compiler": "babel",
|
||||
"tsConfig": "packages/remark-callouts/tsconfig.lib.json",
|
||||
"project": "packages/remark-callouts/package.json",
|
||||
"format": ["esm", "cjs"],
|
||||
"extractCss": true,
|
||||
"generateExportsField": true,
|
||||
"assets": [
|
||||
{
|
||||
"glob": "packages/remark-callouts/README.md",
|
||||
"input": ".",
|
||||
"output": "."
|
||||
},
|
||||
{
|
||||
"glob": "packages/remark-callouts/styles.css",
|
||||
"input": ".",
|
||||
"output": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"publish:dry": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": ["npm publish --dry-run"],
|
||||
"parallel": false,
|
||||
"cwd": "packages/remark-callouts/dist"
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": ["npm publish"],
|
||||
"parallel": false,
|
||||
"cwd": "packages/remark-callouts/dist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
2
packages/remark-callouts/src/index.ts
Normal file
2
packages/remark-callouts/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./lib/remark-callouts";
|
||||
export { default } from "./lib/remark-callouts";
|
||||
83
packages/remark-callouts/src/lib/calloutTypes.ts
Normal file
83
packages/remark-callouts/src/lib/calloutTypes.ts
Normal file
@ -0,0 +1,83 @@
|
||||
export const calloutTypes = {
|
||||
// aliases
|
||||
summary: "abstract",
|
||||
tldr: "abstract",
|
||||
hint: "tip",
|
||||
important: "tip",
|
||||
check: "success",
|
||||
done: "success",
|
||||
help: "question",
|
||||
faq: "question",
|
||||
caution: "warning",
|
||||
attention: "warning",
|
||||
fail: "failure",
|
||||
missing: "failure",
|
||||
error: "danger",
|
||||
cite: "quote",
|
||||
// base types
|
||||
note: {
|
||||
keyword: "note",
|
||||
color: "#448aff",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-pencil"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>',
|
||||
},
|
||||
tip: {
|
||||
keyword: "tip",
|
||||
color: "#00bfa6",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>',
|
||||
},
|
||||
warning: {
|
||||
keyword: "warning",
|
||||
color: "#ff9100",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-alert-triangle"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
|
||||
},
|
||||
abstract: {
|
||||
keyword: "abstract",
|
||||
color: "#00aeff",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-clipboard-list"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M15 2H9a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1z"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>',
|
||||
},
|
||||
info: {
|
||||
keyword: "info",
|
||||
color: "#00b8d4",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
|
||||
},
|
||||
todo: {
|
||||
keyword: "todo",
|
||||
color: "#00b8d4",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-check-circle-2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>',
|
||||
},
|
||||
success: {
|
||||
keyword: "success",
|
||||
color: "#00c853",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-check"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
||||
},
|
||||
question: {
|
||||
keyword: "question",
|
||||
color: "#63dd17",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
|
||||
},
|
||||
failure: {
|
||||
keyword: "failure",
|
||||
color: "#ff5252",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
|
||||
},
|
||||
danger: {
|
||||
keyword: "danger",
|
||||
color: "#ff1745",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>',
|
||||
},
|
||||
bug: {
|
||||
keyword: "bug",
|
||||
color: "#f50057",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-bug"><rect x="8" y="6" width="8" height="14" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>',
|
||||
},
|
||||
example: {
|
||||
keyword: "example",
|
||||
color: "#7c4dff",
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>',
|
||||
},
|
||||
quote: {
|
||||
keyword: "quote",
|
||||
color: "#9e9e9e",
|
||||
svg: '<svg viewBox="0 0 100 100" class="quote-glyph" width="16" height="16"><path fill="currentColor" stroke="currentColor" d="M16.7,13.3c-3.7,0-6.7,3-6.7,6.7v26.7c0,3.7,3,6.7,6.7,6.7h13.5c0.1,6-0.5,18.7-6.3,28.2c0,0,0,0,0,0 c-0.9,1.4-0.7,3.1,0.5,4.2c1.2,1.1,3,1.2,4.3,0.2c0,0,14.7-11.2,14.7-32.7V20c0-3.7-3-6.7-6.7-6.7L16.7,13.3z M63.3,13.3 c-3.7,0-6.7,3-6.7,6.7v26.7c0,3.7,3,6.7,6.7,6.7h13.5c0.1,6-0.5,18.7-6.3,28.2h0c-0.9,1.4-0.7,3.1,0.5,4.2c1.2,1.1,3,1.2,4.3,0.2 c0,0,14.7-11.2,14.7-32.7V20c0-3.7-3-6.7-6.7-6.7L63.3,13.3z"></path></svg>',
|
||||
},
|
||||
};
|
||||
285
packages/remark-callouts/src/lib/remark-callouts.ts
Normal file
285
packages/remark-callouts/src/lib/remark-callouts.ts
Normal file
@ -0,0 +1,285 @@
|
||||
import { visit } from "unist-util-visit";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import type { Plugin } from "unified";
|
||||
import type { Node, Data, Parent } from "unist";
|
||||
import type { Blockquote, Heading, Text, BlockContent } from "mdast";
|
||||
import { parse } from "svg-parser";
|
||||
import { calloutTypes } from "./calloutTypes";
|
||||
|
||||
// escape regex special characters
|
||||
function escapeRegExp(s: string) {
|
||||
return s.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, "g"), "\\$&");
|
||||
}
|
||||
|
||||
// match breaks
|
||||
const find = /[\t ]*(?:\r?\n|\r)/g;
|
||||
|
||||
export const callouts: Plugin = function (providedConfig?: Partial<Config>) {
|
||||
const config: Config = { ...defaultConfig, ...providedConfig };
|
||||
const defaultKeywords: string = Object.keys(config.types)
|
||||
.map(escapeRegExp)
|
||||
.join("|");
|
||||
|
||||
return function (tree) {
|
||||
visit(tree, (node: Node, index, parent: Parent<Node>) => {
|
||||
// Filter required elems
|
||||
if (node.type !== "blockquote") return;
|
||||
|
||||
/** add breaks to text without needing spaces or escapes (turns enters into <br>)
|
||||
* code taken directly from remark-breaks,
|
||||
* see https://github.com/remarkjs/remark-breaks for more info on what this does.
|
||||
*/
|
||||
visit(node, "text", (node: Text, index: number, parent: Parent) => {
|
||||
const result = [];
|
||||
let start = 0;
|
||||
|
||||
find.lastIndex = 0;
|
||||
|
||||
let match = find.exec(node.value);
|
||||
|
||||
while (match) {
|
||||
const position = match.index;
|
||||
|
||||
if (start !== position) {
|
||||
result.push({
|
||||
type: "text",
|
||||
value: node.value.slice(start, position),
|
||||
});
|
||||
}
|
||||
|
||||
result.push({ type: "break" });
|
||||
start = position + match[0].length;
|
||||
match = find.exec(node.value);
|
||||
}
|
||||
|
||||
if (result.length > 0 && parent && typeof index === "number") {
|
||||
if (start < node.value.length) {
|
||||
result.push({ type: "text", value: node.value.slice(start) });
|
||||
}
|
||||
|
||||
parent.children.splice(index, 1, ...result);
|
||||
return index + result.length;
|
||||
}
|
||||
});
|
||||
|
||||
/** add classnames to headings within blockquotes,
|
||||
* mainly to identify when using other plugins that
|
||||
* might interfere. for eg, rehype-auto-link-headings.
|
||||
*/
|
||||
visit(node, "heading", (node) => {
|
||||
const heading = node as Heading;
|
||||
heading.data = {
|
||||
hProperties: {
|
||||
className: "blockquote-heading",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// wrap blockquote in a div
|
||||
const wrapper = {
|
||||
...node,
|
||||
type: "element",
|
||||
tagName: "div",
|
||||
data: {
|
||||
hProperties: {},
|
||||
},
|
||||
children: [node],
|
||||
};
|
||||
|
||||
parent.children.splice(Number(index), 1, wrapper);
|
||||
|
||||
const blockquote = wrapper.children[0] as Blockquote;
|
||||
|
||||
blockquote.data = {
|
||||
hProperties: {
|
||||
className: "blockquote",
|
||||
},
|
||||
};
|
||||
|
||||
// check for callout syntax starts here
|
||||
if (
|
||||
blockquote.children.length <= 0 ||
|
||||
blockquote.children[0].type !== "paragraph"
|
||||
)
|
||||
return;
|
||||
const paragraph = blockquote.children[0];
|
||||
|
||||
if (
|
||||
paragraph.children.length <= 0 ||
|
||||
paragraph.children[0].type !== "text"
|
||||
)
|
||||
return;
|
||||
|
||||
const [t, ...rest] = paragraph.children;
|
||||
|
||||
const regex = new RegExp(
|
||||
`^\\[!(?<keyword>(.*?))\\][\t\f ]?(?<title>.*?)$`,
|
||||
"gi"
|
||||
);
|
||||
const m = regex.exec(t.value);
|
||||
|
||||
// if no callout syntax, forget about it.
|
||||
if (!m) return;
|
||||
|
||||
const [key, title] = [m.groups?.keyword, m.groups?.title];
|
||||
|
||||
// if there's nothing inside the brackets, is it really a callout ?
|
||||
if (!key) return;
|
||||
|
||||
const keyword = key.toLowerCase();
|
||||
const isOneOfKeywords: boolean = new RegExp(defaultKeywords).test(
|
||||
keyword
|
||||
);
|
||||
|
||||
if (title) {
|
||||
const mdast = fromMarkdown(title.trim()).children[0];
|
||||
if (mdast.type === "heading") {
|
||||
mdast.data = {
|
||||
...mdast.data,
|
||||
hProperties: {
|
||||
className: "blockquote-heading",
|
||||
},
|
||||
};
|
||||
}
|
||||
blockquote.children.unshift(mdast as BlockContent);
|
||||
} else {
|
||||
t.value =
|
||||
typeof keyword.charAt(0) === "string"
|
||||
? keyword.charAt(0).toUpperCase() + keyword.slice(1)
|
||||
: keyword;
|
||||
}
|
||||
|
||||
const entry: { [index: string]: string } = {};
|
||||
|
||||
if (isOneOfKeywords) {
|
||||
if (typeof config?.types[keyword] === "string") {
|
||||
const e = String(config?.types[keyword]);
|
||||
Object.assign(entry, config?.types[e]);
|
||||
} else {
|
||||
Object.assign(entry, config?.types[keyword]);
|
||||
}
|
||||
} else {
|
||||
Object.assign(entry, config?.types["note"]);
|
||||
}
|
||||
|
||||
let parsedSvg;
|
||||
|
||||
if (entry && entry.svg) {
|
||||
parsedSvg = parse(entry.svg);
|
||||
}
|
||||
|
||||
// create icon and title node wrapped in div
|
||||
const titleNode: object = {
|
||||
type: "element",
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
data: {
|
||||
hName: "span",
|
||||
hProperties: {
|
||||
style: `color:${entry?.color}`,
|
||||
className: "callout-icon",
|
||||
},
|
||||
hChildren: parsedSvg?.children ? parsedSvg.children : [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
children: title ? [blockquote.children[0]] : [t],
|
||||
data: {
|
||||
hName: "strong",
|
||||
},
|
||||
},
|
||||
],
|
||||
data: {
|
||||
...blockquote.children[0]?.data,
|
||||
hProperties: {
|
||||
className: `${formatClassNameMap(config.classNameMaps.title)(
|
||||
keyword
|
||||
)} ${isOneOfKeywords ? keyword : "note"}`,
|
||||
style: `background-color: ${entry?.color}1a;`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// remove the callout paragraph from the content body
|
||||
if (title) {
|
||||
blockquote.children.shift();
|
||||
}
|
||||
|
||||
if (rest.length > 0) {
|
||||
rest.shift();
|
||||
paragraph.children = rest;
|
||||
} else {
|
||||
blockquote.children.shift();
|
||||
}
|
||||
|
||||
// wrap blockquote content in div
|
||||
const contentNode: object = {
|
||||
type: "element",
|
||||
children: blockquote.children,
|
||||
data: {
|
||||
hProperties: {
|
||||
className: "callout-content",
|
||||
style:
|
||||
parent.type !== "root"
|
||||
? `border-right:1px solid ${entry?.color}33;
|
||||
border-bottom:1px solid ${entry?.color}33;`
|
||||
: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (blockquote.children.length > 0)
|
||||
blockquote.children = [contentNode] as BlockContent[];
|
||||
blockquote.children.unshift(titleNode as BlockContent);
|
||||
|
||||
// Add classes for the callout block
|
||||
blockquote.data = config.dataMaps.block({
|
||||
...blockquote.data,
|
||||
hProperties: {
|
||||
className: formatClassNameMap(config.classNameMaps.block)(
|
||||
keyword.toLowerCase()
|
||||
),
|
||||
style: `border-left-color:${entry?.color};`,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
classNameMaps: {
|
||||
block: ClassNameMap;
|
||||
title: ClassNameMap;
|
||||
};
|
||||
dataMaps: {
|
||||
block: (data: Data) => Data;
|
||||
title: (data: Data) => Data;
|
||||
};
|
||||
types: { [index: string]: string | object };
|
||||
}
|
||||
|
||||
export const defaultConfig: Config = {
|
||||
classNameMaps: {
|
||||
block: "callout",
|
||||
title: "callout-title",
|
||||
},
|
||||
dataMaps: {
|
||||
block: (data) => data,
|
||||
title: (data) => data,
|
||||
},
|
||||
types: { ...calloutTypes },
|
||||
};
|
||||
|
||||
type ClassNames = string | string[];
|
||||
type ClassNameMap = ClassNames | ((title: string) => ClassNames);
|
||||
function formatClassNameMap(gen: ClassNameMap) {
|
||||
return (title: string) => {
|
||||
const classNames = typeof gen == "function" ? gen(title) : gen;
|
||||
return typeof classNames == "object" ? classNames.join(" ") : classNames;
|
||||
};
|
||||
}
|
||||
|
||||
export default callouts;
|
||||
64
packages/remark-callouts/styles.css
Normal file
64
packages/remark-callouts/styles.css
Normal file
@ -0,0 +1,64 @@
|
||||
:root {
|
||||
--callout-bg-color: #f2f3f5;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--callout-bg-color: #161616;
|
||||
}
|
||||
|
||||
.blockquote,
|
||||
.callout {
|
||||
background: #f2f3f5;
|
||||
background: var(--callout-bg-color);
|
||||
font-style: normal;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.callout-title > strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.blockquote,
|
||||
.callout-content {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.blockquote-heading {
|
||||
margin: 5px 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.blockquote > p,
|
||||
.callout-content > p {
|
||||
font-weight: normal;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.callout-title p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.callout-title > strong {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.callout p:before,
|
||||
p:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blockquote > p:before,
|
||||
p:after {
|
||||
display: none;
|
||||
}
|
||||
143
packages/remark-callouts/test/remark-callouts.spec.ts
Normal file
143
packages/remark-callouts/test/remark-callouts.spec.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { expect } from "chai";
|
||||
import { parseDocument } from "htmlparser2";
|
||||
import { selectOne } from "css-select";
|
||||
import { remark } from "remark";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import { callouts, Config } from "../src";
|
||||
|
||||
async function mdToHtml(md: string, options?: Partial<Config>) {
|
||||
return String(
|
||||
await remark()
|
||||
.use(remarkParse)
|
||||
.use(callouts, options)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeStringify)
|
||||
.process(md)
|
||||
);
|
||||
}
|
||||
|
||||
describe("remark callouts", function () {
|
||||
it("parses a blockquote without a callout", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> no callout
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const blockquote = selectOne("div > blockquote.blockquote > p", doc);
|
||||
expect(blockquote).to.have.nested.property("firstChild.data", "no callout");
|
||||
});
|
||||
|
||||
it("parses a blockquote callout with title and content", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!tip]
|
||||
> example content here
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const calloutTitle = selectOne(
|
||||
"div > blockquote.callout > div.callout-title.tip > strong",
|
||||
doc
|
||||
);
|
||||
const calloutContent = selectOne(
|
||||
"div > blockquote.callout > div.callout-content > p",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(calloutTitle).to.have.nested.property("firstChild.data", "Tip");
|
||||
expect(calloutContent).to.have.nested.property(
|
||||
"firstChild.data",
|
||||
"example content here"
|
||||
);
|
||||
});
|
||||
|
||||
it("parses a blockquote callout with case insensitive keyword", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!INFO]
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const calloutTitle = selectOne(
|
||||
"div > blockquote.callout > div.callout-title.info > strong",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(calloutTitle).to.have.nested.property("firstChild.data", "Info");
|
||||
});
|
||||
|
||||
it("parses a blockquote callout with an icon", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!tip]
|
||||
> example content here
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const calloutIcon = selectOne(
|
||||
"div > blockquote.callout > div.callout-title.tip > span.callout-icon > svg",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(calloutIcon).to.exist;
|
||||
});
|
||||
|
||||
it("parses a blockquote callout with a custom title", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!tip] Custom Title
|
||||
> content
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const calloutTitle = selectOne(
|
||||
"div > blockquote.callout > div.callout-title.tip > strong > p",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(calloutTitle).to.have.nested.property(
|
||||
"firstChild.data",
|
||||
"Custom Title"
|
||||
);
|
||||
});
|
||||
|
||||
it("parses a blockquote callout with unknown type to use note", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!xyz]
|
||||
> content
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const calloutTitle = selectOne(
|
||||
"div > blockquote.callout > div.callout-title.note > strong",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(calloutTitle).to.have.nested.property("firstChild.data", "Xyz");
|
||||
});
|
||||
|
||||
it("parses a blockquote callout with unknown type and custom title", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!xyz] Some title
|
||||
> content
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const calloutTitle = selectOne(
|
||||
"div > blockquote.callout > div.callout-title.note > strong > p",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(calloutTitle).to.have.nested.property(
|
||||
"firstChild.data",
|
||||
"Some title"
|
||||
);
|
||||
});
|
||||
|
||||
it("parses a nested blockquote with callout", async function () {
|
||||
const html = await mdToHtml(`\
|
||||
> [!note]
|
||||
> content
|
||||
> > [!info]
|
||||
> > nested callout
|
||||
`);
|
||||
const doc = parseDocument(html);
|
||||
const nestedCallout = selectOne(
|
||||
"div > blockquote.callout > div.callout-content > div > blockquote.callout > div.callout-title > strong",
|
||||
doc
|
||||
);
|
||||
|
||||
expect(nestedCallout).to.have.nested.property("firstChild.data", "Info");
|
||||
});
|
||||
});
|
||||
13
packages/remark-callouts/tsconfig.json
Normal file
13
packages/remark-callouts/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
packages/remark-callouts/tsconfig.lib.json
Normal file
14
packages/remark-callouts/tsconfig.lib.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"types": ["node"],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
20
packages/remark-callouts/tsconfig.spec.json
Normal file
20
packages/remark-callouts/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"types": ["mocha", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user