@lmarcel/highlight
v2.7.1
Published
Editable highlight component for react with support for many languages and custom themes.
Downloads
106
Maintainers
Readme
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
- Installation
- Basic usage
- Advanced usage
- Demonstrations
- Edit mode
- Theming
- Languages
- Plugins
- For authors
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.