facebook / docusaurus

Easy to maintain open source documentation websites.
https://docusaurus.io
MIT License
54.35k stars 8.15k forks source link

Document how to use Prism Plugins / add Prism diff-highlight plugin #3318

Closed rrousselGit closed 3 years ago

rrousselGit commented 3 years ago

📚 Documentation

Prism comes with the ability to use prism plugins to add extra functionalities – such as diff-highlight to combine the syntax highlighting of a language with the diff feature.

It is currently unclear how to enable prism plugins.

I've tried playing with swizzle to extract the code-highlighting renderer, but failed to implement this feature.

Cross-reference:

Have you read the Contributing Guidelines on issues?

Yes

slorber commented 3 years ago

We use react-prism-renderer and it does not seem to support the ability to add such plugin, so unfortunately we can't support this unless they add support, but that seems not planned: https://github.com/FormidableLabs/prism-react-renderer/issues/2

Repraance commented 5 months ago

I found a way to implement diff-highlight using magic comments.

The problem is the line number is wrong, but we can hide it.

Example:

image

markdown

```tsx
// diff-remove
<a href={`/${lng}/some-path`} />
// diff-remove
location.href = `${lng}/some-path`

// diff-add
<a href={'/some-path'} />
// diff-add
location.href = `/some-path`

### Implement 

For `docusaurus.config.js`, set
```js
const config = {
  prism: {
    magicComments: [
      {
        className: 'code-block-diff-add-line',
        line: 'diff-add'
      },
      {
        className: 'code-block-diff-remove-line',
        line: 'diff-remove'
      }
    ]
  }
}

And add custom css

.code-block-diff-add-line {
  background-color: #ccffd8;
  display: block;
  margin: 0 -40px;
  padding: 0 40px;
}

.code-block-diff-add-line::before {
  position: absolute;
  left: 8px;
  padding-right: 8px;
  content: '+';
}

.code-block-diff-remove-line {
  background-color: #ffebe9;
  display: block;
  margin: 0 -40px;
  padding: 0 40px;
}

.code-block-diff-remove-line::before {
  position: absolute;
  left: 8px;
  padding-right: 8px;
  content: '-';
}

/**
 * use magic comments to mark diff blocks
 */
pre code:has(.code-block-diff-add-line) {
  padding-left: 40px!important;
}

pre code:has(.code-block-diff-remove-line) {
  padding-left: 40px!important;
}
thefat32 commented 2 months ago

Implemented diff-highlight compatible with docusaurus (prism-react-renderer)

You need to swizzle in your docusaurus repo:

yarn swizzle @docusaurus/theme-classic prism-include-languages
    prism: {
      theme: prismThemes.github,
      darkTheme: prismThemes.dracula,
      additionalLanguages: ["diff", "diff-ts"],
    },
import siteConfig from "@generated/docusaurus.config";
import type * as PrismNamespace from "prismjs";
import { diffHighlight } from "./prism-diff-highlight";
import "./prism-diff-highlight.css";

const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;

export default function prismIncludeLanguages(
  PrismObject: typeof PrismNamespace
): void {
  const {
    themeConfig: { prism },
  } = siteConfig;
  const { additionalLanguages } = prism as { additionalLanguages: string[] };

  // Prism components work on the Prism instance on the window, while prism-
  // react-renderer uses its own Prism instance. We temporarily mount the
  // instance onto window, import components to enhance it, then remove it to
  // avoid polluting global namespace.
  // You can mutate PrismObject: registering plugins, deleting languages... As
  // long as you don't re-assign it
  globalThis.Prism = PrismObject;

  additionalLanguages.forEach((lang) => {
    const langMatch = DIFF_LANGUAGE_REGEX.exec(lang);
    if (!langMatch) {
      require(`prismjs/components/prism-${lang}`); // not a language specific diff
    } else {
      if (!PrismObject.languages.diff) {
        console.error(
          "prism-include-languages:",
          "You need to import 'diff' language first to use 'diff-xxxx' languages"
        );
      }
      PrismObject.languages[lang] = PrismObject.languages.diff;
    }
  });

  diffHighlight(PrismObject);

  delete globalThis.Prism;
}
import { EnvConfig, PrismLib } from "prism-react-renderer";
import { TokenStream, Token } from "prismjs";

const LANGUAGE_REGEX = /^diff-([\w-]+)/i;

const tokenStreamToString = (tokenStream: TokenStream): string => {
  const result: string[] = [];
  const stack: TokenStream[] = [tokenStream];

  while (stack.length > 0) {
    const item = stack.pop();

    if (typeof item === "string") {
      result.push(item);
    } else if (Array.isArray(item)) {
      for (let i = item.length - 1; i >= 0; i--) {
        stack.push(item[i]);
      }
    } else {
      // If it's a Token, convert it to a string and push it
      stack.push(item.content);
    }
  }

  return result.join("");
};

export function diffHighlight(Prism: PrismLib) {
  Prism.hooks.add("after-tokenize", function (env: EnvConfig) {
    let diffLanguage;
    let diffGrammar;
    const language = env.language;
    if (language !== "diff") {
      const langMatch = LANGUAGE_REGEX.exec(language);
      if (!langMatch) {
        return; // not a language specific diff
      }

      diffLanguage = langMatch[1];
      diffGrammar = Prism.languages[diffLanguage];
      if (!diffGrammar) {
        console.error(
          "prism-diff-highlight:",
          `You need to add language '${diffLanguage}' to use '${language}'`
        );
        return;
      }
    } else return;

    const newTokens = [];
    env.tokens.forEach((token) => {
      if (typeof token === "string") {
        newTokens.push(...Prism.tokenize(token, diffGrammar));
      } else if (token.type === "unchanged") {
        newTokens.push(
          ...Prism.tokenize(tokenStreamToString(token), diffGrammar)
        );
      } else if (["deleted-sign", "inserted-sign"].includes(token.type)) {
        token.alias = [
          token.type === "deleted-sign"
            ? "diff-highlight-deleted"
            : "diff-highlight-inserted",
        ];
        // diff parser always return "deleted" and "inserted" lines with content of type array
        if (token.content.length > 1) {
          const newTokenContent: Array<string | Token> = [];
          // preserve prefixes and don't parse them again
          // subTokens from diff parser are of type Token
          (token.content as Array<string | Token>).forEach(
            (subToken: Token) => {
              if (subToken.type === "prefix") {
                newTokenContent.push(subToken);
              } else {
                newTokenContent.push(
                  ...Prism.tokenize(tokenStreamToString(subToken), diffGrammar)
                );
              }
            }
          );
          token.content = newTokenContent;
        }
        newTokens.push(token);
      } else if (token.type === "coord") {
        newTokens.push(token);
      }
    });
    console.log(newTokens);
    env.tokens = newTokens;
  });
}

code .token.diff-highlight-inserted { background-color: rgba(0, 255, 128, .1); }

code .token.coord { font-weight: 700; }


- Result:

![image](https://github.com/facebook/docusaurus/assets/10998637/a7d3351e-738a-4047-8bfc-d73c52e26f18)

You can add whatever other language you want in `additionalLanguages`:

```ts
      additionalLanguages: ["diff", "diff-ts", "powershell", "diff-powershell"],

image

NOTE FOR DEVS/INTERESTED PEOPLE:

Prism plugins do not work with prism-react-renderer because they rely in other hooks that are not called. They only call hooks "before-tokenize" and "after-tokenize". This reimplementation uses "after-tokenize" to edit tokens generated by prism-react-renderer tokenizer.

In theory every plugin could be re-implemented using those hooks to work with prism-react-renderer

EDIT: Code Update using typescript EDIT2: Fix edge cases EDIT3: Add support for "coord" token EDIT4: Don't use recursion in tokenStreamToString

uc-kywong commented 1 week ago

image

I think you can write you own component to display diff like github.

$ npm install --save react-diff-view

I am using version 3.2.1. (https://www.npmjs.com/package/react-diff-view)


Create your component

src/components/CustomDiff/CustomDiff.js

import {Decoration, Diff, Hunk, parseDiff} from "react-diff-view";
import 'react-diff-view/style/index.css';
import './CustomDiff.css';

export default function CustomDiff({diffText}) {
  const files = parseDiff(diffText);
  return (
    <div>
      {files.map((
        {hunks, newPath, oldPath}, i) =>
        <Diff
          renderToken={(token) => token.content}
          key={i}
          viewType="split"
          diffType=""
          hunks={hunks}
        >
          {
            hunks => hunks.map(hunk => {
              return <>
                <Decoration key={'decoration-' + hunk.content}>
                  {`${oldPath}`}
                </Decoration>
                <Decoration key={'decoration-' + hunk.content}>
                  {`${newPath} - ${hunk.content}`}
                </Decoration>
                <Hunk key={hunk.content} hunk={hunk} />
              </>
            }
          )}
        </Diff>
      )}
    </div>
  );
}

Css for your component.

src/components/CustomDiff/CustomDiff.css

:root {
    --ifm-table-border-width: 0px;
}

table {
    display: table;
}

.diff-code {
    font-size: 85%;
}

How to use it in markdown?

import CustomDiff from "@site/src/components/CustomDiff/CustomDiff";

<CustomDiff diffText={`
diff --git a/package.json b/package.json
--- a/package.json  (revision c38f44524532d3a87986bfa4b9b8b82d8f0e29a0)
+++ b/package.json  (revision 93e71e98e457dae88403cdf204a8f4a85f60cc42)
@@ -1,6 +1,6 @@
 {
   "name": "SSO-Admin-UI",
-  "version": "1.0.0",
+  "version": "2.0.0-alpha.0",
   "license": "MIT",
   "scripts": {
     "start": "ng serve -o -c=dev --port=8500 --baseHref=/",
`}
/>