├── .gitignore ├── .storybook ├── addons.js └── config.js ├── README.md ├── build ├── Parser.d.ts ├── Parser.js ├── TextareaDecorator.d.ts ├── TextareaDecorator.js ├── helpers.d.ts ├── helpers.js ├── highlight.d.ts ├── highlight.js ├── index.d.ts └── index.js ├── package.json ├── src ├── Parser.ts ├── TextareaDecorator.ts ├── helpers.ts ├── highlight.ts └── index.ts ├── stories ├── SimpleTextarea.js ├── generic.html ├── index.js ├── index.stories.js ├── parser.js ├── roygbiv.html ├── style.css └── wysiwyg.html ├── styles.css ├── tsconfig.json ├── types.d.ts ├── yarn-error.log └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules 4 | storybook-static 5 | yarn-error.log 6 | yarn.log -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../stories', true, /.stories.js$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lightweight Decorator for Textareas 2 | 3 | ``` 4 | yarn add light-code-editor 5 | # or 6 | npm install light-code-editor 7 | ``` 8 | 9 | # How to use [codesandbox](https://codesandbox.io/s/k91x8593w3) 10 | 11 | 1. include styles "light-code-editor/styles.css" 12 | 2. create parser 13 | 14 | ```js 15 | const genericParser = new Parser({ 16 | whitespace: /\s+/, 17 | comment: /\/\*([^\*]|\*[^\/])*(\*\/?)?|(\/\/|#)[^\r\n]*/, 18 | string: /"(\\.|[^"\r\n])*"?|'(\\.|[^'\r\n])*'?/, 19 | number: /0x[\dA-Fa-f]+|-?(\d+\.?\d*|\.\d+)/, 20 | keyword: /(and|as|case|catch|class|const|def|delete|die|do|else|elseif|esac|exit|extends|false|fi|finally|for|foreach|function|global|if|new|null|or|private|protected|public|published|resource|return|self|static|struct|switch|then|this|throw|true|try|var|void|while|xor)(?!\w|=)/, 21 | variable: /[\$\%\@](\->|\w)+(?!\w)|\${\w*}?/, 22 | define: /[$A-Z_a-z0-9]+/, 23 | op: /[\+\-\*\/=<>!]=?|[\(\)\{\}\[\]\.\|]/, 24 | other: /\S+/ 25 | }); 26 | ``` 27 | 28 | 3. style your items 29 | 30 | ```css 31 | .ltd .comment { 32 | color: red; 33 | } 34 | 35 | .ltd .keyword { 36 | color: black; 37 | } 38 | ``` 39 | 40 | ```js 41 | var textarea = $("codeArea"); 42 | textarea.value = "\n\n\t" + "\n"; 43 | decorator = new TextareaDecorator(textarea, parser); 44 | ``` 45 | 46 | ```js 47 | var textarea = $("codeArea"); 48 | textarea.value = "\n\n\t" + "\n"; 49 | decorator = new TextareaDecorator(textarea, parser); 50 | bindKey(textarea, { 51 | "Ctrl-1": e => { 52 | insertAtCursor("your superb text", el); 53 | decorator.update(); 54 | }, 55 | "Shift-Ctrl-2": e => { 56 | alert("hello"); 57 | } 58 | }); 59 | ``` 60 | 61 | see detailed examples in stories 62 | 63 | ## In browser live syntax highlighting 64 | 65 |
 66 | <!-- normal textarea fall-back, add an id to access it from javascript -->
 67 | <textarea id='codeArea' class='ldt'></textarea>
 68 | <noscript>Please enable JavaScript to allow syntax highlighting.</noscript>
 69 | 
70 | 71 | ### JS 72 | 73 |
 74 | // create a parser with a mapping of css classes to regular expressions
 75 | // everything must be matched, so 'whitespace' and 'other' are commonly included
 76 | var parser = new Parser(
 77 |   { whitespace: /\s+/,
 78 |     comment: /\/\/[^\r\n]*/,
 79 |     other: /\S+/ } );
 80 | // get the textarea with $ (document.getElementById)
 81 | // pass the textarea element and parser to LDT
 82 | var ldt = new TextareaDecorator( $('codeArea'), parser );
 83 | 
84 | 85 | ### CSS 86 | 87 |
 88 | /* editor styles */
 89 | .ldt {
 90 | 	width: 400px;
 91 | 	height: 300px;
 92 | 	border: 1px solid black;
 93 | }
 94 | /* styles applied to comment tokens */
 95 | .ldt .comment {
 96 |     color: silver;
 97 | }
 98 | 
99 | 100 | ## API 101 | 102 | ### TextareaDecorator 103 | 104 | - `new TextareaDecorator( textarea, parser )` Converts a HTML `textarea` element into an auto highlighting TextareaDecorator. `parser` is used to determine how to subdivide and style the content. `parser` can be any object which defines the `tokenize` and `identify` methods as described in the Parser API below. 105 | - `.input` The input layer of the LDT, a `textarea` element. 106 | - `.output` The output layer of the LDT, a `pre` element. 107 | - `.update()` Updates the highlighting of the LDT. It is automatically called on user input. You shouldn't need to call this unless you programmatically changed the contents of the `textarea`. 108 | 109 | ### Parser 110 | 111 | - `new Parser( [rules], [i] )` Creates a parser. `rules` is an object whose keys are CSS classes and values are the regular expressions which match each token. `i` is a boolean which determines if the matching is case insensitive, it defaults to `false`. 112 | - `.add( rules )` Adds a mapping of CSS class names to regular expressions. 113 | - `.tokenize( string )` Splits `string` into an array of tokens as defined by `.rules`. 114 | - `.identify( string )` Finds the CSS class name associated with the token `string`. 115 | 116 | ### Keybinder 117 | 118 | This is a singleton, you do not need to instantiate this object. 119 | 120 | - `.bindKey( element, [keymap] )` Adds Keybinder methods to `element`, optionally setting the element's `keymap`. 121 | 122 | ### SelectHelper 123 | 124 | This is a singleton, you do not need to instantiate this object. 125 | 126 | - `.add( element )` Adds SelectHelper methods to `element`. 127 | - `element.insertAtCursor( string )` Inserts `string` into the `element` before the current cursor position. 128 | 129 | ### Contributions 130 | Opened to contributions, it would be nice to have some predefined parsers and intergrations for react, vue, etc 131 | -------------------------------------------------------------------------------- /build/Parser.d.ts: -------------------------------------------------------------------------------- 1 | interface RegExpMap { 2 | [k: string]: RegExp; 3 | } 4 | export declare class Parser { 5 | i: string; 6 | parseRE: RegExp; 7 | ruleSrc: string[]; 8 | ruleMap: RegExpMap; 9 | constructor(rules: RegExpMap, i: string); 10 | tokenize: (input: string) => RegExpMatchArray; 11 | add: (rules: RegExpMap) => void; 12 | identify: (token: string) => string; 13 | } 14 | export {}; 15 | -------------------------------------------------------------------------------- /build/Parser.js: -------------------------------------------------------------------------------- 1 | export class Parser { 2 | constructor(rules, i) { 3 | this.tokenize = (input) => input.match(this.parseRE); 4 | this.add = (rules) => { 5 | for (var rule in rules) { 6 | var s = rules[rule].source; 7 | this.ruleSrc.push(s); 8 | this.ruleMap[rule] = new RegExp("^(" + s + ")$", this.i); 9 | } 10 | this.parseRE = new RegExp(this.ruleSrc.join("|"), "g" + this.i); 11 | }; 12 | this.identify = (token) => { 13 | for (var rule in this.ruleMap) { 14 | if (this.ruleMap[rule].test(token)) { 15 | return rule; 16 | } 17 | } 18 | }; 19 | this.i = i ? "i" : ""; 20 | this.parseRE = null; 21 | this.ruleSrc = []; 22 | this.ruleMap = {}; 23 | this.add(rules); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /build/TextareaDecorator.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from "./Parser"; 2 | export declare class TextareaDecorator { 3 | textarea: HTMLTextAreaElement; 4 | parser: Parser; 5 | output: HTMLPreElement; 6 | parent: HTMLDivElement; 7 | constructor(textarea: HTMLTextAreaElement, parser: Parser, className?: string); 8 | update: () => void; 9 | } 10 | -------------------------------------------------------------------------------- /build/TextareaDecorator.js: -------------------------------------------------------------------------------- 1 | import { highlightPreText } from "./highlight"; 2 | export class TextareaDecorator { 3 | constructor(textarea, parser, className) { 4 | this.textarea = textarea; 5 | this.parser = parser; 6 | this.update = () => { 7 | var input = this.textarea.value; 8 | if (input) { 9 | highlightPreText(input, this.output, this.parser); 10 | var lines = input.split("\n"); 11 | var maxlen = 0; 12 | var curlen; 13 | for (var i = 0; i < lines.length; i++) { 14 | var tabLength = 0, offset = -1; 15 | while ((offset = lines[i].indexOf("\t", offset + 1)) > -1) { 16 | tabLength += 7 - ((tabLength + offset) % 8); 17 | } 18 | curlen = lines[i].length + tabLength; 19 | maxlen = maxlen > curlen ? maxlen : curlen; 20 | } 21 | this.textarea.cols = maxlen + 1; 22 | this.textarea.rows = lines.length + 1; 23 | } 24 | else { 25 | this.output.innerHTML = ""; 26 | this.textarea.cols = this.textarea.rows = 1; 27 | } 28 | }; 29 | this.update = this.update.bind(this); 30 | var parent = document.createElement("div"); 31 | parent.classList.add(className); 32 | var output = document.createElement("pre"); 33 | parent.appendChild(output); 34 | var label = document.createElement("label"); 35 | parent.appendChild(label); 36 | textarea.parentNode.replaceChild(parent, textarea); 37 | label.appendChild(textarea); 38 | parent.className = "ldt " + textarea.className; 39 | textarea.className = ""; 40 | textarea.spellcheck = false; 41 | textarea.wrap = "off"; 42 | textarea.addEventListener("input", this.update, false); 43 | this.output = output; 44 | this.parent = parent; 45 | this.update(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /build/helpers.d.ts: -------------------------------------------------------------------------------- 1 | export declare function bindKey(element: HTMLElement, keymap: any): void; 2 | export declare function insertAtCursor(x: string, element: HTMLTextAreaElement): void; 3 | -------------------------------------------------------------------------------- /build/helpers.js: -------------------------------------------------------------------------------- 1 | const keyNames = { 2 | 8: "Backspace", 3 | 9: "Tab", 4 | 13: "Enter", 5 | 16: "Shift", 6 | 17: "Ctrl", 7 | 18: "Alt", 8 | 19: "Pause", 9 | 20: "CapsLk", 10 | 27: "Esc", 11 | 33: "PgUp", 12 | 34: "PgDn", 13 | 35: "End", 14 | 36: "Home", 15 | 37: "Left", 16 | 38: "Up", 17 | 39: "Right", 18 | 40: "Down", 19 | 45: "Insert", 20 | 46: "Delete", 21 | 112: "F1", 22 | 113: "F2", 23 | 114: "F3", 24 | 115: "F4", 25 | 116: "F5", 26 | 117: "F6", 27 | 118: "F7", 28 | 119: "F8", 29 | 120: "F9", 30 | 121: "F10", 31 | 122: "F11", 32 | 123: "F12", 33 | 145: "ScrLk" 34 | }; 35 | function keyEventNormalizer(e, keymap) { 36 | e = e || window.event; 37 | var query = ""; 38 | e.shiftKey && (query += "Shift-"); 39 | e.ctrlKey && (query += "Ctrl-"); 40 | e.altKey && (query += "Alt-"); 41 | e.metaKey && (query += "Meta-"); 42 | var key = e.which || e.keyCode || e.charCode; 43 | if (keyNames[key]) 44 | query += keyNames[key]; 45 | else 46 | query += String.fromCharCode(key).toUpperCase(); 47 | if (keymap[query] && keymap[query]()) { 48 | e.preventDefault && e.preventDefault(); 49 | e.stopPropagation && e.stopPropagation(); 50 | return false; 51 | } 52 | return true; 53 | } 54 | export function bindKey(element, keymap) { 55 | var fireOnKeyPress = true; 56 | element.onkeydown = e => { 57 | fireOnKeyPress = false; 58 | return keyEventNormalizer(e, keymap); 59 | }; 60 | element.onkeypress = e => { 61 | if (fireOnKeyPress) 62 | return keyEventNormalizer(e, keymap); 63 | fireOnKeyPress = true; 64 | return true; 65 | }; 66 | } 67 | export function insertAtCursor(x, element) { 68 | var s = element.selectionStart, e = element.selectionEnd, v = element.value; 69 | element.value = v.substring(0, s) + x + v.substring(e); 70 | s += x.length; 71 | element.setSelectionRange(s, s); 72 | } 73 | -------------------------------------------------------------------------------- /build/highlight.d.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from "./Parser"; 2 | export declare function highlightPreText(input: string, output: HTMLPreElement, parser: Parser): void; 3 | -------------------------------------------------------------------------------- /build/highlight.js: -------------------------------------------------------------------------------- 1 | export function highlightPreText(input, output, parser) { 2 | var oldTokens = output.childNodes; 3 | var newTokens = parser.tokenize(input); 4 | var firstDiff, lastDiffNew, lastDiffOld; 5 | for (firstDiff = 0; firstDiff < newTokens.length && firstDiff < oldTokens.length; firstDiff++) 6 | if (newTokens[firstDiff] !== oldTokens[firstDiff].textContent) 7 | break; 8 | while (newTokens.length < oldTokens.length) 9 | output.removeChild(oldTokens[firstDiff]); 10 | for (lastDiffNew = newTokens.length - 1, lastDiffOld = oldTokens.length - 1; firstDiff < lastDiffOld; lastDiffNew--, lastDiffOld--) 11 | if (newTokens[lastDiffNew] !== oldTokens[lastDiffOld].textContent) 12 | break; 13 | for (; firstDiff <= lastDiffOld; firstDiff++) { 14 | const el = oldTokens[firstDiff]; 15 | el.className = parser.identify(newTokens[firstDiff]); 16 | el.textContent = el.innerText = newTokens[firstDiff]; 17 | } 18 | for (var insertionPt = oldTokens[firstDiff] || null; firstDiff <= lastDiffNew; firstDiff++) { 19 | var span = document.createElement("span"); 20 | span.className = parser.identify(newTokens[firstDiff]); 21 | span.textContent = span.innerText = newTokens[firstDiff]; 22 | output.insertBefore(span, insertionPt); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /build/index.d.ts: -------------------------------------------------------------------------------- 1 | export { Parser } from "./Parser"; 2 | export { TextareaDecorator } from "./TextareaDecorator"; 3 | export { highlightPreText } from "./highlight"; 4 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | export { Parser } from "./Parser"; 2 | export { TextareaDecorator } from "./TextareaDecorator"; 3 | export { highlightPreText } from "./highlight"; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "light-code-editor", 3 | "version": "1.0.6", 4 | "repository": { 5 | "url": "https://github.com/zhDmitry/light-code-editor", 6 | "type": "github" 7 | }, 8 | "main": "./build/index.js", 9 | "module": "./build/index.js", 10 | "files": [ 11 | "build/**", 12 | "src/**", 13 | "styles.css", 14 | "package.json", 15 | "README.md", 16 | "types.d.ts" 17 | ], 18 | "author": { 19 | "email": "kraken@live.ru", 20 | "name": "Dmitry", 21 | "url": "zhd.js.org" 22 | }, 23 | "types": "./types.d.ts", 24 | "devDependencies": { 25 | "@babel/core": "^7.1.5", 26 | "@storybook/addon-actions": "^4.0.4", 27 | "@storybook/addon-links": "^4.0.4", 28 | "@storybook/addons": "^4.0.4", 29 | "@storybook/react": "^4.0.4", 30 | "babel-loader": "^8.0.4", 31 | "typescript": "^3.1.6", 32 | "gh-pages": "^2.0.1", 33 | "react": "^16.6.1", 34 | "react-dom": "^16.6.1" 35 | }, 36 | "dependencies": { 37 | }, 38 | "scripts": { 39 | "storybook": "start-storybook -p 6006", 40 | "build-storybook": "build-storybook", 41 | "build": "tsc --build", 42 | "deploy": "gh-pages -d storybook-static" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Parser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Generates a tokenizer from regular expressions for TextareaDecorator 3 | */ 4 | 5 | interface RegExpMap { 6 | [k: string]: RegExp; 7 | } 8 | export class Parser { 9 | i: string; 10 | parseRE: RegExp; 11 | ruleSrc: string[]; 12 | ruleMap: RegExpMap; 13 | constructor(rules: RegExpMap, i: string) { 14 | this.i = i ? "i" : ""; 15 | this.parseRE = null; 16 | this.ruleSrc = []; 17 | this.ruleMap = {}; 18 | 19 | this.add(rules); 20 | } 21 | tokenize = (input: string) => input.match(this.parseRE); 22 | 23 | add = (rules: RegExpMap) => { 24 | for (var rule in rules) { 25 | var s = rules[rule].source; 26 | this.ruleSrc.push(s); 27 | this.ruleMap[rule] = new RegExp("^(" + s + ")$", this.i); 28 | } 29 | this.parseRE = new RegExp(this.ruleSrc.join("|"), "g" + this.i); 30 | }; 31 | identify = (token: string) => { 32 | for (var rule in this.ruleMap) { 33 | if (this.ruleMap[rule].test(token)) { 34 | return rule; 35 | } 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/TextareaDecorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Builds and maintains a styled output layer under a textarea input layer 3 | */ 4 | import { Parser } from "./Parser"; 5 | import { highlightPreText } from "./highlight"; 6 | 7 | export class TextareaDecorator { 8 | output: HTMLPreElement; 9 | parent: HTMLDivElement; 10 | 11 | constructor( 12 | public textarea: HTMLTextAreaElement, 13 | public parser: Parser, 14 | className?: string 15 | ) { 16 | // construct editor DOM 17 | this.update = this.update.bind(this); 18 | 19 | var parent = document.createElement("div"); 20 | parent.classList.add(className); 21 | var output = document.createElement("pre"); 22 | parent.appendChild(output); 23 | var label = document.createElement("label"); 24 | parent.appendChild(label); 25 | // replace the textarea with RTA DOM and reattach on label 26 | textarea.parentNode.replaceChild(parent, textarea); 27 | label.appendChild(textarea); 28 | // transfer the CSS styles to our editor 29 | parent.className = "ldt " + textarea.className; 30 | textarea.className = ""; 31 | // turn off built-in spellchecking in firefox 32 | textarea.spellcheck = false; 33 | // turn off word wrap 34 | textarea.wrap = "off"; 35 | textarea.addEventListener("input", this.update, false); 36 | this.output = output; 37 | this.parent = parent; 38 | this.update(); 39 | } 40 | 41 | update = () => { 42 | var input = this.textarea.value; 43 | if (input) { 44 | highlightPreText(input, this.output, this.parser); 45 | // determine the best size for the textarea 46 | var lines = input.split("\n"); 47 | // find the number of columns 48 | var maxlen = 0; 49 | var curlen: any; 50 | for (var i = 0; i < lines.length; i++) { 51 | // calculate the width of each tab 52 | var tabLength = 0, 53 | offset = -1; 54 | while ((offset = lines[i].indexOf("\t", offset + 1)) > -1) { 55 | tabLength += 7 - ((tabLength + offset) % 8); 56 | } 57 | curlen = lines[i].length + tabLength; 58 | // store the greatest line length thus far 59 | maxlen = maxlen > curlen ? maxlen : curlen; 60 | } 61 | this.textarea.cols = maxlen + 1; 62 | this.textarea.rows = lines.length + 1; 63 | } else { 64 | // clear the display 65 | this.output.innerHTML = ""; 66 | // reset textarea rows/cols 67 | this.textarea.cols = this.textarea.rows = 1; 68 | } 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | const keyNames: { [k: number]: string } = { 2 | 8: "Backspace", 3 | 9: "Tab", 4 | 13: "Enter", 5 | 16: "Shift", 6 | 17: "Ctrl", 7 | 18: "Alt", 8 | 19: "Pause", 9 | 20: "CapsLk", 10 | 27: "Esc", 11 | 33: "PgUp", 12 | 34: "PgDn", 13 | 35: "End", 14 | 36: "Home", 15 | 37: "Left", 16 | 38: "Up", 17 | 39: "Right", 18 | 40: "Down", 19 | 45: "Insert", 20 | 46: "Delete", 21 | 112: "F1", 22 | 113: "F2", 23 | 114: "F3", 24 | 115: "F4", 25 | 116: "F5", 26 | 117: "F6", 27 | 118: "F7", 28 | 119: "F8", 29 | 120: "F9", 30 | 121: "F10", 31 | 122: "F11", 32 | 123: "F12", 33 | 145: "ScrLk" 34 | }; 35 | 36 | function keyEventNormalizer(e: KeyboardEvent, keymap: any) { 37 | // get the event object and start constructing a query 38 | e = e || (window.event as any); 39 | var query = ""; 40 | // add in prefixes for each key modifier 41 | e.shiftKey && (query += "Shift-"); 42 | e.ctrlKey && (query += "Ctrl-"); 43 | e.altKey && (query += "Alt-"); 44 | e.metaKey && (query += "Meta-"); 45 | // determine the key code 46 | var key = e.which || e.keyCode || e.charCode; 47 | // if we have a name for it, use it 48 | if (keyNames[key]) query += keyNames[key]; 49 | // otherwise turn it into a string 50 | else query += String.fromCharCode(key).toUpperCase(); 51 | // try to run the keybinding, cancel the event if it returns true 52 | if (keymap[query] && keymap[query]()) { 53 | e.preventDefault && e.preventDefault(); 54 | e.stopPropagation && e.stopPropagation(); 55 | return false; 56 | } 57 | return true; 58 | } 59 | 60 | export function bindKey(element: HTMLElement, keymap: any) { 61 | // capture onkeydown and onkeypress events to capture repeating key events 62 | // maintain a boolean so we only fire once per character 63 | var fireOnKeyPress = true; 64 | element.onkeydown = e => { 65 | fireOnKeyPress = false; 66 | return keyEventNormalizer(e, keymap); 67 | }; 68 | element.onkeypress = e => { 69 | if (fireOnKeyPress) return keyEventNormalizer(e, keymap); 70 | fireOnKeyPress = true; 71 | return true; 72 | }; 73 | } 74 | 75 | export function insertAtCursor(x: string, element: HTMLTextAreaElement) { 76 | var s = element.selectionStart, 77 | e = element.selectionEnd, 78 | v = element.value; 79 | element.value = v.substring(0, s) + x + v.substring(e); 80 | s += x.length; 81 | element.setSelectionRange(s, s); 82 | } 83 | -------------------------------------------------------------------------------- /src/highlight.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from "./Parser"; 2 | 3 | export function highlightPreText( 4 | input: string, 5 | output: HTMLPreElement, 6 | parser: Parser 7 | ) { 8 | var oldTokens = output.childNodes; 9 | var newTokens = parser.tokenize(input); 10 | var firstDiff, lastDiffNew, lastDiffOld; 11 | // find the first difference 12 | for ( 13 | firstDiff = 0; 14 | firstDiff < newTokens.length && firstDiff < oldTokens.length; 15 | firstDiff++ 16 | ) 17 | if (newTokens[firstDiff] !== oldTokens[firstDiff].textContent) break; 18 | // trim the length of output nodes to the size of the input 19 | while (newTokens.length < oldTokens.length) 20 | output.removeChild(oldTokens[firstDiff]); 21 | // find the last difference 22 | for ( 23 | lastDiffNew = newTokens.length - 1, lastDiffOld = oldTokens.length - 1; 24 | firstDiff < lastDiffOld; 25 | lastDiffNew--, lastDiffOld-- 26 | ) 27 | if (newTokens[lastDiffNew] !== oldTokens[lastDiffOld].textContent) break; 28 | // update modified spans 29 | for (; firstDiff <= lastDiffOld; firstDiff++) { 30 | const el: HTMLElement = oldTokens[firstDiff] as any; 31 | el.className = parser.identify(newTokens[firstDiff]); 32 | el.textContent = el.innerText = newTokens[firstDiff]; 33 | } 34 | // add in modified spans 35 | for ( 36 | var insertionPt = oldTokens[firstDiff] || null; 37 | firstDiff <= lastDiffNew; 38 | firstDiff++ 39 | ) { 40 | var span = document.createElement("span"); 41 | span.className = parser.identify(newTokens[firstDiff]); 42 | span.textContent = span.innerText = newTokens[firstDiff]; 43 | output.insertBefore(span, insertionPt); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Parser } from "./Parser"; 2 | export { TextareaDecorator } from "./TextareaDecorator"; 3 | export { highlightPreText } from "./highlight"; 4 | -------------------------------------------------------------------------------- /stories/SimpleTextarea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./style.css"; 3 | export default class extends React.Component { 4 | static defaultProps = { 5 | is: "textarea" 6 | }; 7 | ref = React.createRef(); 8 | componentDidMount() { 9 | this.props.onMount(this.ref.current); 10 | console.log("this.ref.current: ", this.ref.current); 11 | } 12 | render() { 13 | const { is: Is, onMount, ...props } = this.props; 14 | return ( 15 |
23 | 24 |
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stories/generic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LDT Generic Syntax Highlighter Demo 6 | 7 | 8 | 18 | 19 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import "../build"; 2 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { document, console } from "global"; 3 | import { storiesOf } from "@storybook/react"; 4 | import { genericParser, wisywygParser } from "./parser"; 5 | import { TextareaDecorator, highlightPreText } from "../build"; 6 | import { bindKey, insertAtCursor } from "../build/helpers"; 7 | 8 | import Textarea from "./SimpleTextarea"; 9 | 10 | storiesOf("Demo", module) 11 | .add("generic", () => { 12 | return ( 13 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /stories/style.css: -------------------------------------------------------------------------------- 1 | /* demo stylesheet, makes a full page .input */ 2 | @import "../styles.css"; 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | html, 9 | body, 10 | .input { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | body { 16 | padding: 0.5em; 17 | box-sizing: border-box; 18 | -moz-box-sizing: border-box; 19 | -webkit-box-sizing: border-box; 20 | } 21 | 22 | /* pretty textarea */ 23 | .input { 24 | border: 1px solid #aaa; 25 | box-sizing: border-box; 26 | -moz-box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | box-shadow: 0 4px 8px #eee inset; 29 | -moz-box-shadow: 0 4px 8px #eee inset; 30 | -webkit-box-shadow: 0 4px 8px #eee inset; 31 | border-radius: 4px; 32 | -moz-border-radius: 4px; 33 | -webkit-border-radius: 4px; 34 | } 35 | 36 | /* this match with your names in parser */ 37 | pre .comment { 38 | color: silver; 39 | } 40 | pre .string { 41 | color: green; 42 | } 43 | pre .number { 44 | color: navy; 45 | } 46 | /* setting inline-block and margin to avoid misalignment bug in windows */ 47 | pre .keyword { 48 | font-weight: bold; 49 | display: inline-block; 50 | margin-bottom: -1px; 51 | } 52 | pre .variable { 53 | color: cyan; 54 | } 55 | pre .define { 56 | color: blue; 57 | } 58 | 59 | pre .bold { 60 | font-weight: bold; 61 | } 62 | pre .underline { 63 | text-decoration: underline; 64 | } 65 | pre .italic { 66 | font-style: italic; 67 | } 68 | pre .strike { 69 | text-decoration: line-through; 70 | } 71 | /* fix for bold/italic/non regular fonts on windows */ 72 | pre .fix { 73 | display: inline-block; 74 | margin-bottom: -1px; 75 | } 76 | -------------------------------------------------------------------------------- /stories/wysiwyg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LDT WYSIWYG Demo 6 | 7 | 8 | 17 | 18 | 19 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* TextareaDecorator.css 2 | * written by Colin Kuebler 2012 3 | * Part of LDT, dual licensed under GPLv3 and MIT 4 | * Provides styles for rendering a textarea on top of a pre with scrollbars 5 | */ 6 | 7 | /* settings you can play with */ 8 | 9 | .ldt, 10 | .ldt label { 11 | padding: 4px; 12 | } 13 | 14 | .ldt, 15 | .ldt pre, 16 | .ldt textarea { 17 | font-size: 16px !important; 18 | /* resize algorithm depends on a monospaced font */ 19 | font-family: monospace !important; 20 | color: black; 21 | } 22 | 23 | .ldt textarea { 24 | color: rgba(0, 0, 0, 0.2) !important; 25 | } 26 | 27 | /* settings you shouldn't play with unless you have a good reason */ 28 | 29 | .ldt { 30 | overflow: auto; 31 | width: 100%; 32 | height: 100%; 33 | position: relative; 34 | } 35 | 36 | .ldt pre { 37 | margin: 0; 38 | overflow: initial; 39 | } 40 | 41 | .ldt label { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | display: inline; 46 | box-sizing: border-box; 47 | -moz-box-sizing: border-box; 48 | -webkit-box-sizing: border-box; 49 | cursor: text; 50 | } 51 | 52 | .ldt textarea { 53 | margin: 0; 54 | padding: 0; 55 | border: 0; 56 | background: 0; 57 | outline: none; 58 | resize: none; 59 | min-width: 100%; 60 | min-height: 100%; 61 | overflow: hidden; 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": ["es6", "esnext", "dom"], 7 | "strict": true /* Enable all strict type-checking options. */, 8 | "outDir": "./build", 9 | "removeComments": true, 10 | "declaration": true, 11 | "strictNullChecks": false, 12 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 13 | }, 14 | "include": ["./src/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./build/index"; 2 | export * from "./build/helpers"; 3 | --------------------------------------------------------------------------------