Skip to content

textarea with syntax highlighting powered by solid-js and vscode-oniguruma

License

Notifications You must be signed in to change notification settings

bigmistqke/tm-textarea

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tm-textarea

đź“„ tm-textarea

pnpm

Textarea with syntax highlighting powered by solid-js and vscode-oniguruma.

tm-textarea.mp4

Table of Contents

Installation

npm i tm tm-textarea
# or
yarn add tm tm-textarea
# or
pnpm add tm tm-textarea

Custom Element (tm-textarea)

The main export is a custom-element <tm-textarea/> powered by @lume/element

Attribute/Property Types
import type { Grammar, Theme } from 'tm-textarea/tm'

interface tmTextareaAttributes extends ComponentProps<'div'> {
  grammar?: Grammar
  theme?: Theme
  value?: string
  editable?: boolean
  stylesheet?: string | CSSStyleSheet
  onInput?: (event: InputEvent & { currentTarget: tmTextareaElement }) => void
}

Usage

import 'tm-textarea'

import { setCDN } from 'tm-textarea/cdn'
setCDN('/tm')

export default () => (
  <tm-textarea
    grammar="tsx"
    theme="andromeeda"
    value="const sum = (a: string, b: string) => a + b"
    editable={true}
    style={{
      padding: '10px',
      'font-size': '16pt',
    }}
    stylesheet="code, code * { font-style:normal; }"
    onInput={e => console.log(e.currentTarget.value)}
  />
)

Styling The Custom Element

Some DOM ::part() are exported.

  • root exposes root container.
  • code exposes the code tag.
  • line exposes the lines.
  • textarea exposes textarea to maybe change the selection color.
tm-textarea {
  min-height: 100%;
  min-width: 100%;
  padding: 10px;
  line-height: 16pt;
}

/* overwrite the theme background-color */
tm-textarea::part(root) {
  background: transparent;
  /* set a  color for meanwhile the theme loads */
  color: grey;
}

/* overwrite the selected text background color */
tm-textarea::part(textarea)::selection {
  background: deepskyblue;
}

/* add line-numbers */
tm-textarea::part(line)::before {
  display: inline-block;
  counter-reset: variable calc(var(--line-number) + 1);
  min-width: 7ch;
  content: counter(variable);
}

tm-textarea::part(textarea) {
  margin-left: 7ch;
}

The attribute stylesheet could be used as a last resort to customize the theme. In the following example we avoid italics in the rendered coded. The stylesheet is created, cached and possibly reused on the different tm-textarea instances.

<tm-textarea
  grammar="tsx"
  theme="andromeeda"
  value="const sum = (a: string, b: string) => a + b"
  stylesheet="code, code * { font-style: normal; }"
/>

Solid Component (tm-textarea/solid)

A solid component of tm-textarea is available at tm-textarea/solid

Prop Types
import { Grammar, Theme } from 'tm-textarea/tm'

interface tmTextareaProps extends Omit<ComponentProps<'div'>, 'style'> {
  grammar: Grammar
  theme: Theme
  value: string
  editable?: boolean
  style?: JSX.CSSProperties
  onInput?: (event: InputEvent & { currentTarget: HTMLTextAreaElement }) => void
}

Usage

import { TmTextarea } from 'tm-textarea/solid'

export default () => (
  <TmTextarea
    grammar="tsx"
    theme="min-light"
    value="const sum = (a: string, b: string) => a + b"
    editable={true}
    style={{
      padding: '10px',
      'font-size': '16pt',
    }}
    onInput={e => console.log(e.currentTarget.value)}
  />
)

CDN (tm-textarea/cdn)

To ease development we provide a way to set themes/grammars by setting the theme or grammar property with a string. Without configuration these are resolved to tm-themes and tm-grammars hosted on esm.sh.

To provide a way to customize how these keys are resolved we provide a global function setCDN, exported from tm-textarea. This function accepts as arguments either a base-url or a callback-function.

When given a base-url, this will be used to fetch

  • ${cdn}/tm-themes/themes/${theme}.json for the themes
  • ${cdn}/tm-grammars/grammars/${grammar}.json for the grammars
  • ${cdn}/vscode-oniguruma/release/onig.wasm for the oniguruma wasm-file

When given a callback, the returned string will be used to fetch instead.

Usage

import { setCDN } from 'tm-textarea/cdn'

// Set absolute base-url
setCDN('https://unpkg.com')

// Set relative base-url (for local hosting)
setCDN('/assets/tm')

// Use the callback-form
setCDN((type, id) => (type === 'oniguruma' ? `./oniguruma.wasm` : `./${type}/${id}.json`))

Themes & Grammars (tm-textarea/tm)

We export a list of textmate grammars and themes that are hosted on tm-grammars and tm-themes. These are used internally and maintained by shiki.

import type { Theme, Grammar } from 'tm-textarea/tm'
import { themes, grammars } from 'tm-textarea/tm'

Bindings

In addition to the core functionality, tm-textarea provides bindings that enhance the text editing experience by introducing keyboard shortcuts and behaviors that are common in code editors.

TabIndentation (tm-textarea/bindings/tab-indentation)

The TabIndentation binding enables tab and shift-tab indentation for a native textarea, tm-textarea or <TmTextarea/>. It allows users to easily increase or decrease the indentation level of lines or selected blocks of text.

Type Definitions for TabIndentation
interface TabIndentation {
  /** Adds event listeners to the passed element for handling 'keydown' and 'input' events specific to indentation. */
  binding: (element: HTMLTextAreaElement | TmTextareaElement) => () => void;
  /** Dispatches `formatIndent` and `formatOutdent` event-types when pressing tab */
  onKeyDown: (event: KeyboardEvent & { currentTarget: TmTextareaElement | HTMLTextAreaElement }) => void;
  /** Add indentation on `formatIndent` and `formatOutdent` event-type. */
  onInput: (event: InputEvent & { currentTarget: TmTextareaElement | HTMLTextAreaElement }) => void;
  /** Format leading whitespace of given string according to given tab-size. */
  format: (source: string, tabSize: number) => string;
  /** Utilities */
  getLeadingWhitespace: (source: string) => string;
  getLineStart: (value: string, position: number) => number;
  getIndentationSegments: (leadingWhitespace: string, tabSize: number) => string[];
}

Importing and Usage

import { TmTextarea } from 'tm-textarea/solid'
import { TabIndentation } from 'tm-textarea/bindings/tab-indentation'
import source from "./source"

const App = () => {
  return (
    <TmTextarea
      ref={TabIndentation.binding}
      value={TabIndentation.format(source, 2)}
      grammar="tsx"
      theme="andromeeda"
    />
  )
}

export default App

Line Numbers

To keep the implementation of tm-textarea as generic as possible, we do not provide specific props/attributes to render line-numbers. Instead css-variables are set to assist with the rendering of css line-numbers:

  • --tm-line-number: the line number of a single line. This variable is set on tm-textarea::part(line) (custom element) and .tm-textarea pre (solid component).
  • --tm-line-digits: the amount of digits of the current line-count, useful for preventing overflowing line-numbers.

It can get a bit involved to account for all the possible edge cases, so we do provide the following css-snippets that you can use as a base:

Custom Element CSS Snippet

.line-numbers::part(root) {
  /* Calculate the offset from the digits of the current line-count and an additional ch for left-padding. */
  --offset: calc(var(--tm-line-digits) * 1ch + 1ch);
}

/* Render a pseudo before-element in each line. */
.line-numbers::part(line)::before {
  /* Position absolute to not offset the line's content. */
  position: absolute;
  /* Counter line's offset with the reversed offset. */
  transform: translateX(calc(var(--offset) * -1));
  /* Sets a counter line-number with the css-variable --tm-line-number + 1. */
  counter-reset: line-number calc(var(--tm-line-number) + 1);
  /* Adds the counter to the pseudo-element's content. */
  content: counter(line-number);
}

/* Offset the textarea and the lines. */
.line-numbers::part(line),
.line-numbers::part(textarea) {
  /* Offset should not be done with margin/padding to prevent conflict with inlined tab-calculations. */
  transform: translateX(var(--offset));
}

Solid Component CSS Snippet

.line-numbers {
  /* Calculate the offset from the digits of the current line-count and an additional ch for left-padding. */
  --width: calc(var(--tm-line-digits) * 1ch + 1ch);
}

/* Render a pseudo before-element in each line. */
.line-numbers pre::before {
    /* Position absolute to not offset the line's content. */
  position: absolute;
  /* Counter line's offset with the reversed offset. */
  transform: translateX(calc(var(--offset) * -1));
  /* Sets a counter line-number with the css-variable --tm-line-number + 1. */
  counter-reset: line-number calc(var(--tm-line-number) + 1);
  /* Adds the counter to the pseudo-element's content. */
  content: counter(line-number);
}

/* Offset the textarea and the lines. */
.line-numbers pre,
.line-numbers textarea {
  /* Offset should not be done with margin/padding to prevent conflict with inlined tab-calculations. */
  transform: translateX(var(--line-number-width));
}

About

textarea with syntax highlighting powered by solid-js and vscode-oniguruma

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published