@lmarcel/highlight
TypeScript icon, indicating that this package has built-in type declarations

2.7.1 • Public • Published

Editable highlight component for react with support for many languages ​​and custom themes.

It is now possible to create the definition of languages ​​that do not exist! That's right, you can make your own! See the demonstration. Imports, language definitions and the documentation have also been updated. All the changes I wanted have already been made, any problems, please create an issue.

Summary

Features

  • Support for many languages
  • Loads the dependencies of each language alone
  • Editable content support
  • Mobile editable content support
  • Customizable existing or created language definitions
  • Customizable theming
  • Tab navigation support
  • Included styles
  • With built-in line number viewer
  • Deep integration with TypeScript
  • Plug and play! You don't need an external configuration to use it

Installation

To install you need to run in your project:

//Using pnpm
pnpm add @lmarcel/highlight

//Using npm
npm install @lmarcel/highlight

//Using yarn
yarn add @lmarcel/highlight

Basic usage

It's very simple to use!

<Highlight
  theme="oneDark"
  language="ts"
  code={`import path from "path";
console.log(path.resolve(__dirname, "test"));`}
/>

Advanced usage

You can edit! That's right, like a normal textarea! See the demo.

import { useState } from "react";
import { Highlight, EditEvent } from "@lmarcel/highlight";

export default function Home() {
  const [code, setCode] = useState(`const a = "red";\nconsole.log(a);`);

  function handleOnEdit(e: EditEvent) {
    setCode(e.currentTarget.value);
  };

  return (
    <Highlight
      placeholder="Put your code here..."
      style={{
        minWidth: 800
      }}
      editable={true}
      onEdit={handleOnEdit}
      code={code}
      language="javascript"
    />
  );
};

Demonstrations

See some demos at storybook. It has an editable demonstration!

Edit mode

If the editable property is true, the user will be able to enter the edit mode, where it is possible to change the content inside the component.

Tab navigation

In edit mode, the tab key inserts a tab space (as in a normal editor). To exit edit mode, just press esc key or click outside the component (triggering a blur event).

However, if the user chooses to leave using the esc key, a focus event will be triggered in the component, so that the component does not harm navigation by tab navigation.

When the component is focused, it is possible to enter edit mode by pressing the enter key.

I thought of showing the available keys in a kind of menu, but this could harm your design and the current structure makes it very difficult to do this within the component itself, so I left it free for you to choose how you want to indicate the navigation possibilities to the user.

I left two functions that can be passed for this purpose: onEnterEditMode and onExitEditMode.

Theming

You can define pre-existing or custom themes for the component.

Available themes

I left some predefined themes, some with more extended support for some languages.

Extensive themes (I made several changes):

  • oneDark
  • oneLight
  • laserwave
  • dracula

Updated themes (I made small changes):

  • vsDark
  • vsLight

Old themes (not changed):

  • byverduDracula
  • duotoneDark
  • duotoneLight
  • github
  • nightOwl
  • nightOwlLight
  • oceanicNext
  • palenight
  • okaidia
  • shadesOfPurple
  • synthwave84
  • ultramin

Custom themes

You can also edit existing themes in a very simple way using HighlightCustomTheme. Or even create your own themes.

import { HighlightCustomTheme, themes } from "@lmarcel/highlight";

export const myTheme = new HighlightCustomTheme(/*...*/);

export const anotherTheme = new HighlightCustomTheme.extends(myTheme, /*...*/);

export const storybookTheme = HighlightCustomTheme.extends(themes.oneDark, {
  numbersBorderColor: "#1ea7fd",
  backgroundColor: "#272727",
  numbersBackgroundColor: "#2b2a2a",
  numbersColor: "#cfcfcf"
});

Languages

This library uses Prism.js to generate the tokens for each language component, to avoid ambiguity I call these components languages definitions.

Available languages

This library currently supports ALL Prism.js languages ​​dynamically. Because it was too big and not feasible to do manually, I migrated this list to storybook in available languages.

Custom languages definitions

It is possible, but quite complex, to edit language definitions using the library. You can make your own (it inevitably requires extensive knowledge of regex, see the demonstration):

//imports
import { Highlight, HighlightCustomLanguage } from "@lmarcel/highlight";

//my custom language definitions
const banner = new HighlightCustomLanguage(
  "myBanner",
  ["banner"],
  {
    grammar: {
      "banners": [{
        pattern: /\btitle\b/g,
        alias: "banner-title"
      }, {
        pattern: /\bsubtitle\b/g,
        alias: "banner-subtitle"
      }, {
        pattern: /\bend\b/g,
        alias: "banner-end"
      }]
    },
  }
);

//my javascript language definitions
const javascript = new HighlightCustomLanguage(
  "javascript",
  [],
  {
    grammar: "javascript",
  }
);

javascript.replaceTokenRule(
  "keyword",
  "control-flow",
  (oldToken) => {
    return {
      ...oldToken,
      pattern:
        /\b(?<!\.)(?:(await(?= |\()|break(?=\b)|catch(?=[\s]*\()|continue(?=\b)|do(?=\b)|else(?=\b)|finally(?=\b)|for(?=\b)|if(?=\b)|return(?=\b)|switch(?=\b)|throw(?=\b)|try(?=\b)|while(?=\b)|yield(?=\b)))/,
      alias: "control-flow",
    };
  }
);

export { javascript, banner };

//component
<Highlight
  externalLanguages={[banner, javascript]}
  language="banner"
  code={code}
/>

The definitions are available in the grammar property of the new language instance.

When creating a new instance it is possible to pass the raw value of this property, another instance of the class or even the name (but not an alias) of a pre-existing language (inheriting the language definitions).

This is what happens in this section:

const javascript = new HighlightCustomLanguage(
  "javascript",
  [],
  {
    grammar: "javascript",
  }
);

Within grammar, the mapping of tokens is done, each token has one or more rules within it.

These rules can have an alias, which is the value that can be passed after the token to be used in styling.

I left some functions available in the instance to manipulate these tokens. But it is a very complex resource and I may have missed something.

javascript.replaceTokenRule(
  "keyword", //token
  "control-flow", //token rule alias
  (oldToken) => { //function to return the new token rule
    return {
      ...oldToken,

      //regex
      pattern: 
        /\b(?<!\.)(?:(await(?= |\()|break(?=\b)|catch(?=[\s]*\()|continue(?=\b)|do(?=\b)|else(?=\b)|finally(?=\b)|for(?=\b)|if(?=\b)|return(?=\b)|switch(?=\b)|throw(?=\b)|try(?=\b)|while(?=\b)|yield(?=\b)))/,

      //token rule alias
      alias: "control-flow", 
    };
  }
);

Plugins

One of the most complex and most useful parts. A single plugin can make changes to code, theme, tokens, property of rendered lines and other things. The library itself already comes with a very simple plugin that I created for testing, the corePlugin.

Currently its only function is to style spaces generated by tabs.

See how to use:

import { corePlugin, Highlight } from "@lmarcel/highlight";

export function Code(/* ... */) {
  return (
    <Highlight
      plugins={[
        corePlugin()
      ]}
      /* ... */
    />
  );
}

Plugins are executed in the same order they are passed to the component.

The plugin must always be passed as a function, because it can receive specific settings from it. Avoid using too many plugins simultaneously, it can hurt performance and some can conflict.

Here is an example of the structure of a plugin:

//packages/highlight/src/plugins/custom/corePlugin.ts
export const corePlugin = HighlightPlugin.create<CorePluginSettings>(
  {
    codeLine: (settings, tokens, core) => {
      const spaces = core.tabSize ?? 2;

      if (
        settings?.showTabulations &&
        tokensStartWithTabulation(tokens, spaces)
      ) {
        /* ... */
      }

      return tokens;
    },
    theme: (settings, theme, core) => {
      if (settings?.showTabulations) {
        /* ... */
      }

      return theme;
    },
    code: (settings, code, core) => {
      let numberOfTabsLastLine = 0;

      if (settings?.showTabulations) {
        /* ... */
      }

      return code;
    },
  },
  {
    showTabulations: true,
  }
);

The functions follow the same pattern. The first parameter is the plugin's settings, the second is the value it wants to change and return and the last is the main properties passed to the component.

Does not support async functions!

I left a demo of this plugin. I made something very simple, so take it easy if you find fault!

For authors

For those who are interested, I left a page on storybook containing some examples of how it is possible to create plugins, language definitions and themes.

See the codes.

I also left a lot of helpful comments on the properties I typed within the code. If you are going to use TypeScript you will see occasionally. I apologize if this part is not very well documented.

Recommendation

A library that can be very useful for building complex regex is magic regex. It is a new library with a lot of potential.

Package Sidebar

Install

npm i @lmarcel/highlight

Weekly Downloads

3

Version

2.7.1

License

MIT

Unpacked Size

410 kB

Total Files

7

Last publish

Collaborators

  • lmarcel