Comparing version 0.2.3 to 0.3.0
import type { Root } from 'hast'; | ||
export declare function getColor(classList: string[]): string | undefined; | ||
export declare type Parsable = [string, string] | { | ||
@@ -6,7 +7,40 @@ lang: string; | ||
}; | ||
export interface Parsed<T extends Char> { | ||
export interface Parsed<T extends Char = Char, L extends Line = Line> { | ||
language: string; | ||
lines: L[]; | ||
chars: T[]; | ||
} | ||
export declare function tokenColors(code: Parsed<Char> | Parsable): [string, [number, number], string?][]; | ||
export interface Tokenized<T extends Token = Token, L extends Line = Line> { | ||
language: string; | ||
lines: L[]; | ||
tokens: T[]; | ||
} | ||
export interface Line { | ||
tags: string[]; | ||
number?: number; | ||
} | ||
export interface Text { | ||
classList: string[]; | ||
sections: string[]; | ||
color?: string; | ||
background?: string; | ||
provinance?: 'create' | 'retain' | 'delete'; | ||
} | ||
export interface Char extends Text { | ||
char: string; | ||
token: [number, number]; | ||
} | ||
export interface Token extends Text { | ||
token: string; | ||
prior?: [number, number]; | ||
location: [number, number]; | ||
} | ||
export declare function parse(language: string, code: string): Parsed<Char>; | ||
export declare const tagRegex: RegExp; | ||
/** | ||
* @internal | ||
* @param input - parsed or parsable variable | ||
* @returns parsed code | ||
*/ | ||
export declare function ensureParsed(input: Parsed | Parsable): Parsed; | ||
export declare function ready(): Promise<{ | ||
@@ -19,21 +53,8 @@ flagToScope: (flag: string) => string | undefined; | ||
export declare function toString(code: Parsed<Char> | Parsable): string; | ||
export declare function parse(language: string, code: string): Parsed<Char>; | ||
export interface Char { | ||
char: string; | ||
classList: string[]; | ||
token: [number, number]; | ||
} | ||
export interface RepChar extends Char { | ||
from: 'new' | 'old'; | ||
} | ||
export interface FormChar extends Char { | ||
from: 'create' | 'keep' | 'delete'; | ||
} | ||
export declare function substitute(code: Parsed<Char> | Parsable, subs: Record<string, string>): Parsed<RepChar>; | ||
export declare function transform(code: Parsed<Char> | Parsable, start: Record<string, string>, final: Record<string, string>): FormChar[]; | ||
export interface Transition { | ||
delete: [string, [number, number], string?][]; | ||
create: [string, [number, number], string?][]; | ||
retain: [string, [number, number], [number, number], string?][]; | ||
} | ||
export declare function transition(code: Parsed<Char> | Parsable, start: Record<string, string>, final: Record<string, string>): Transition; | ||
export declare function getSpan(tree: Char[], at: number): string; | ||
/** | ||
* Removes all code-fns tags. | ||
* | ||
* @param code - the parsed or parsable code to clean | ||
*/ | ||
export declare function clean(code: Parsed<Char> | Parsable): Parsed<Char>; |
407
lib/code.js
@@ -6,64 +6,18 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.transition = exports.transform = exports.substitute = exports.parse = exports.toString = exports.ready = exports.tokenColors = void 0; | ||
exports.clean = exports.getSpan = exports.toString = exports.ready = exports.ensureParsed = exports.tagRegex = exports.parse = exports.getColor = void 0; | ||
const starry_night_1 = require("@wooorm/starry-night"); | ||
const dark_style_json_1 = __importDefault(require("./dark-style.json")); | ||
const rules = new Map(Object.entries(dark_style_json_1.default).map(([k, v]) => [k, new Map(Object.entries(v))])); | ||
function ensureParsed(input) { | ||
if (Array.isArray(input)) { | ||
return parse(input[0], input[1]); | ||
} | ||
else if ('code' in input) { | ||
return parse(input.lang, input.code); | ||
} | ||
else { | ||
return input; | ||
} | ||
function getColor(classList) { | ||
console.assert(classList.length <= 1, `classList too long`); | ||
const styles = classList.length === 1 ? rules.get(`.${classList[0]}`) : new Map(); | ||
console.assert((styles?.size ?? 0) <= 1, `more styles than just color`); | ||
const color = styles?.get('color'); | ||
return color; | ||
} | ||
function tokenColors(code) { | ||
const input = ensureParsed(code).chars; | ||
const result = []; | ||
let lastColor = Symbol(); | ||
let [ln, at] = [0, 0]; | ||
for (let i = 0; i < input.length; i++) { | ||
const classList = input[i].classList; | ||
console.assert(classList.length <= 1, `classList too long`); | ||
const styles = classList.length === 1 ? rules.get(`.${classList[0]}`) : new Map(); | ||
console.assert((styles?.size ?? 0) <= 1, `more styles than just color`); | ||
const color = styles?.get('color'); | ||
if (input[i].char === '\n') { | ||
lastColor = Symbol(); | ||
ln++; | ||
at = 0; | ||
} | ||
else if (color === lastColor) { | ||
last(result)[0] += input[i].char; | ||
at++; | ||
} | ||
else { | ||
const char = input[i].char; | ||
result.push(color ? [char, [ln, at], color] : [char, [ln, at]]); | ||
at++; | ||
} | ||
lastColor = color; | ||
} | ||
return result; | ||
} | ||
exports.tokenColors = tokenColors; | ||
let starryNight = null; | ||
const starryNightPromise = (0, starry_night_1.createStarryNight)(starry_night_1.all); | ||
starryNightPromise.then((sn) => (starryNight = sn)); | ||
function ready() { | ||
return starryNightPromise; | ||
} | ||
exports.ready = ready; | ||
function toString(code) { | ||
const parsed = ensureParsed(code); | ||
const result = []; | ||
parsed.chars.forEach(({ char }) => result.push(char)); | ||
return result.join(''); | ||
} | ||
exports.toString = toString; | ||
exports.getColor = getColor; | ||
function parse(language, code) { | ||
if (starryNight == null) | ||
if (starryNight == null) { | ||
throw new Error('you must await ready() to initialize package'); | ||
} | ||
const scope = starryNight.flagToScope(language); | ||
@@ -75,13 +29,9 @@ if (typeof scope !== 'string') { | ||
const converted = recurse(parsed); | ||
return { | ||
return markTree({ | ||
language, | ||
lines: [], | ||
chars: converted, | ||
}; | ||
}); | ||
} | ||
exports.parse = parse; | ||
function last(arr) { | ||
if (arr.length === 0) | ||
arr.push([]); | ||
return arr[arr.length - 1]; | ||
} | ||
function recurse(node, classes = [], result = []) { | ||
@@ -106,2 +56,3 @@ if (node.type === 'element') { | ||
token: [i, node.value.length], | ||
sections: [], | ||
}); | ||
@@ -112,3 +63,126 @@ } | ||
} | ||
const tagRegex = /^\/\*<[^\S\r\n]*(.*?)[^\S\r\n]*>\*\/$/; | ||
const nextLineRegex = /^\/\/:[^\S\n]*next-line[^\S\n]+([^\n]+?)[^\S\n]*$/; | ||
const thisLineRegex = /^\/\/:[^\S\n]*this-line[^\S\n]+([^\n]+?)[^\S\n]*$/; | ||
const blockStartRegex = /^\/\/<<[^\S\n]*([^\n]+?)[^\S\n]*$/; | ||
const blockEndRegex = /^\/\/>>[^\S\n]*$/; | ||
const sectionStartRegex = /^\/\*<<[^\S\n]*([^\n]+?)[^\S\n]*\*\/$/; | ||
const sectionEndRegex = /^\/\*>>[^\S\n]*\*\/$/; | ||
exports.tagRegex = /^\/\*<[^\S\r\n]*(.*?)[^\S\r\n]*>\*\/$/; | ||
const specialTypes = [ | ||
[nextLineRegex, ['nextLine', 'line']], | ||
[thisLineRegex, ['thisLine', 'line']], | ||
[blockStartRegex, ['blockStart', 'line']], | ||
[blockEndRegex, ['blockEnd', 'line']], | ||
[sectionStartRegex, ['sectionStart', 'span']], | ||
[sectionEndRegex, ['sectionEnd', 'span']], | ||
[exports.tagRegex, ['tag', 'span']], | ||
]; | ||
function getSpecialType(span) { | ||
for (const [regex, result] of specialTypes) { | ||
if (regex.test(span)) | ||
return result; | ||
} | ||
return []; | ||
} | ||
function* spans(chars) { | ||
let i = 0; | ||
while (i < chars.length) { | ||
const char = chars[i]; | ||
console.assert(char.token[0] === 0, `token was not beginning of span`); | ||
yield [chars.slice(i, i + char.token[1]), i]; | ||
i += char.token[1]; | ||
} | ||
} | ||
function markTree(parsed) { | ||
const chars = parsed.chars; | ||
const lineCount = 1 + | ||
chars.reduce((prior, { char }) => { | ||
return prior + (char === '\n' ? 1 : 0); | ||
}, 0); | ||
const lines = new Array(lineCount) | ||
.fill(1) | ||
.map(() => ({ tags: [] })); | ||
let ln = 0; | ||
const blocks = []; | ||
const sections = []; | ||
let sectionHold = null; | ||
const result = []; | ||
for (let i = 0; i < chars.length; i++) { | ||
const char = chars[i]; | ||
if (char.token[0] === 0 && sectionHold != null) { | ||
sections.unshift(sectionHold); | ||
sectionHold = null; | ||
} | ||
result.push({ | ||
...char, | ||
sections: [...sections], | ||
}); | ||
if (char.token[0] !== 0) { | ||
continue; | ||
} | ||
const span = getSpan(chars, i); | ||
if (char.char === '\n') { | ||
ln++; | ||
lines[ln].tags.push(...blocks); | ||
} | ||
else if (char.classList.length === 1 && char.classList[0] === 'pl-c') { | ||
if (nextLineRegex.test(span)) { | ||
const name = span.match(nextLineRegex)?.[1]; | ||
lines[ln + 1].tags.push(name); | ||
} | ||
else if (thisLineRegex.test(span)) { | ||
const name = span.match(thisLineRegex)?.[1]; | ||
lines[ln].tags.push(name); | ||
} | ||
else if (blockStartRegex.test(span)) { | ||
blocks.push(span.match(blockStartRegex)?.[1]); | ||
} | ||
else if (blockEndRegex.test(span)) { | ||
blocks.pop(); | ||
} | ||
else if (sectionStartRegex.test(span)) { | ||
sectionHold = span.match(sectionStartRegex)?.[1]; | ||
} | ||
else if (sectionEndRegex.test(span)) { | ||
sections.shift(); | ||
} | ||
} | ||
} | ||
return { | ||
...parsed, | ||
lines, | ||
chars: result, | ||
}; | ||
} | ||
/** | ||
* @internal | ||
* @param input - parsed or parsable variable | ||
* @returns parsed code | ||
*/ | ||
function ensureParsed(input) { | ||
if (Array.isArray(input)) { | ||
return parse(input[0], input[1]); | ||
} | ||
else if (typeof input === 'object' && 'code' in input) { | ||
return parse(input.lang, input.code); | ||
} | ||
else { | ||
return input; | ||
} | ||
} | ||
exports.ensureParsed = ensureParsed; | ||
let starryNight = null; | ||
const starryNightPromise = (0, starry_night_1.createStarryNight)(starry_night_1.all); | ||
starryNightPromise.then((sn) => (starryNight = sn)); | ||
function ready() { | ||
return starryNightPromise; | ||
} | ||
exports.ready = ready; | ||
function toString(code) { | ||
const parsed = ensureParsed(code); | ||
const result = []; | ||
parsed.chars.forEach(({ char }) => result.push(char)); | ||
return result.join(''); | ||
} | ||
exports.toString = toString; | ||
function getSpan(tree, at) { | ||
@@ -124,171 +198,54 @@ const [back, length] = tree[at].token; | ||
} | ||
function substitute(code, subs) { | ||
exports.getSpan = getSpan; | ||
/** | ||
* Removes all code-fns tags. | ||
* | ||
* @param code - the parsed or parsable code to clean | ||
*/ | ||
function clean(code) { | ||
const parsed = ensureParsed(code); | ||
const language = parsed.language; | ||
const tree = parsed.chars; | ||
const replacements = []; | ||
let final = ''; | ||
tree.forEach((char, at) => { | ||
if (char.token[0] !== 0) | ||
return; | ||
const span = getSpan(tree, at); | ||
if (char.classList[0] === 'pl-c' && tagRegex.test(span)) { | ||
const [, name] = span.match(tagRegex); | ||
if (name in subs) { | ||
const rep = subs[name]; | ||
final += rep; | ||
if (rep !== '') | ||
replacements.push([at, rep.length]); | ||
} | ||
else { | ||
final += span; | ||
} | ||
const result = []; | ||
let lineNumber = 0; | ||
let finalNumber = 1; | ||
let lineSkip = false; | ||
const lines = []; | ||
for (const [span] of spans(parsed.chars)) { | ||
if (span.length === 1 && span[0].char === '\n') | ||
lineNumber++; | ||
if (lineSkip) { | ||
console.assert(span.length === 1 && span[0].char === '\n', `expected a new line`); | ||
lineSkip = false; | ||
continue; | ||
} | ||
else { | ||
final += span; | ||
} | ||
}); | ||
const reparsed = parse(language, final); | ||
let [r, ri] = [0, 0]; | ||
let inReplacement = false; | ||
return { | ||
language, | ||
chars: reparsed.chars.map((char, at) => { | ||
if (inReplacement) { | ||
ri++; | ||
if (ri === replacements[r][1]) { | ||
inReplacement = false; | ||
r++; | ||
} | ||
} | ||
else if (r < replacements.length) { | ||
const [rat] = replacements[r]; | ||
if (rat === at) { | ||
inReplacement = true; | ||
} | ||
} | ||
return { | ||
...char, | ||
from: inReplacement ? 'new' : 'old', | ||
}; | ||
}), | ||
}; | ||
} | ||
exports.substitute = substitute; | ||
function transform(code, start, final) { | ||
const tree = ensureParsed(code); | ||
const before = substitute(tree, start); | ||
const after = substitute(tree, final); | ||
let [bat] = [0]; | ||
let [aat] = [0]; | ||
const chars = []; | ||
while (bat < before.chars.length || aat < after.chars.length) { | ||
const bchar = before.chars[bat] ?? null; | ||
const achar = after.chars[aat] ?? null; | ||
if (bchar?.from === 'old' && achar?.from === 'old') { | ||
chars.push({ | ||
...achar, | ||
from: 'keep', | ||
else if (span.length === 1 && span[0].char === '\n') { | ||
lines.push({ | ||
...parsed.lines[lineNumber], | ||
number: finalNumber, | ||
}); | ||
bat++; | ||
aat++; | ||
finalNumber++; | ||
} | ||
else if (bchar?.from === 'new') { | ||
chars.push({ | ||
...bchar, | ||
from: 'delete', | ||
}); | ||
bat++; | ||
console.assert(span[0].classList.length === 1, `multiple classes found`); | ||
const specialTypes = getSpecialType(span.reduce((text, { char }) => text + char, '')); | ||
if (span[0].classList[0] !== 'pl-c' || specialTypes.length === 0) { | ||
result.push(...span); | ||
} | ||
else if (achar?.from === 'new') { | ||
chars.push({ | ||
...achar, | ||
from: 'create', | ||
}); | ||
aat++; | ||
else if (specialTypes.includes('line')) { | ||
lineSkip = true; | ||
} | ||
} | ||
return chars; | ||
} | ||
exports.transform = transform; | ||
function transition(code, start, final) { | ||
const tree = ensureParsed(code); | ||
const chars = transform(tree, start, final); | ||
const result = { | ||
delete: [], | ||
create: [], | ||
retain: [], | ||
lines.push({ | ||
...parsed.lines[lineNumber], | ||
number: finalNumber, | ||
}); | ||
if (lineSkip) { | ||
lines.pop(); | ||
console.assert(result[result.length - 1].char === '\n', `expected a new line`); | ||
result.pop(); | ||
} | ||
return { | ||
...parsed, | ||
chars: result, | ||
lines, | ||
}; | ||
let [dln, dat] = [0, 0]; | ||
let [cln, cat] = [0, 0]; | ||
let lastColor = Symbol(); | ||
let lastFrom = Symbol(); | ||
chars.forEach((char) => { | ||
const classList = char.classList; | ||
console.assert(classList.length <= 1, `classList too long`); | ||
const styles = classList.length === 1 ? rules.get(`.${classList[0]}`) : new Map(); | ||
console.assert((styles?.size ?? 0) <= 1, `more styles than just color`, styles); | ||
const color = styles?.get('color'); | ||
if (char.char === '\n') { | ||
if (char.from === 'keep' || char.from === 'create') { | ||
cln++; | ||
cat = 0; | ||
} | ||
if (char.from === 'keep' || char.from === 'delete') { | ||
dln++; | ||
dat = 0; | ||
} | ||
lastColor = Symbol(); | ||
lastFrom = Symbol(); | ||
} | ||
else if (color === lastColor && char.from === lastFrom) { | ||
if (char.from === 'delete') { | ||
last(result.delete)[0] += char.char; | ||
dat++; | ||
} | ||
else if (char.from === 'create') { | ||
last(result.create)[0] += char.char; | ||
cat++; | ||
} | ||
else if (char.from === 'keep') { | ||
last(result.retain)[0] += char.char; | ||
dat++; | ||
cat++; | ||
} | ||
lastFrom = char.from; | ||
lastColor = color; | ||
} | ||
else { | ||
if (char.from === 'delete') { | ||
result.delete.push([ | ||
char.char, | ||
[dln, dat], | ||
...(color ? [color] : []), | ||
]); | ||
dat++; | ||
} | ||
else if (char.from === 'create') { | ||
result.create.push([ | ||
char.char, | ||
[cln, cat], | ||
...(color ? [color] : []), | ||
]); | ||
cat++; | ||
} | ||
else if (char.from === 'keep') { | ||
result.retain.push([ | ||
char.char, | ||
[dln, dat], | ||
[cln, cat], | ||
...(color ? [color] : []), | ||
]); | ||
dat++; | ||
cat++; | ||
} | ||
lastFrom = char.from; | ||
lastColor = color; | ||
} | ||
}); | ||
return result; | ||
} | ||
exports.transition = transition; | ||
exports.clean = clean; |
{ | ||
"name": "code-fns", | ||
"version": "0.2.3", | ||
"version": "0.3.0", | ||
"description": "A library for visualizing code.", | ||
@@ -21,3 +21,3 @@ "license": "MIT", | ||
"scripts": { | ||
"test": "vitest --ui", | ||
"test": "vitest", | ||
"dev": "vite", | ||
@@ -28,3 +28,4 @@ "build": "tsc", | ||
"prettier": "prettier --write .", | ||
"lint": "eslint ./src/" | ||
"lint": "eslint ./src/", | ||
"coverage": "vitest run --coverage" | ||
}, | ||
@@ -35,5 +36,7 @@ "devDependencies": { | ||
"@typescript-eslint/parser": "^5.36.1", | ||
"@vitest/coverage-c8": "^0.22.1", | ||
"@vitest/ui": "^0.22.1", | ||
"css": "^3.0.0", | ||
"eslint": "^8.23.0", | ||
"eslint-plugin-tsdoc": "^0.2.16", | ||
"prettier": "^2.7.1", | ||
@@ -40,0 +43,0 @@ "typescript": "^4.6.4", |
@@ -14,3 +14,3 @@ # code-fns | ||
Most code highlighters in JavaScript rely on HTML and CSS. When working outside | ||
of a standard webpage, however, these formats become difficult to use. Code-fns | ||
of a standard web page, however, these formats become difficult to use. Code-fns | ||
is domain-agnostic, and will export tokens as plain objects to be converted to | ||
@@ -37,25 +37,9 @@ whatever format you choose. Specifically, code-fns was built for use in the | ||
```tsx | ||
import { ready, tokenColors } from 'code-fns'; | ||
import { ready, color } from 'code-fns'; | ||
await ready(); | ||
const tokens = tokenColors(['tsx', '() => true']); | ||
const code = color(['tsx', '() => true']); | ||
``` | ||
You will receive an array of tokens, which are themselves a tuple of a string, a | ||
location, and a color, when applicable. Colors are based on the github dark | ||
theme, though we hope to add more themes in the future. | ||
```tsx | ||
// tokens | ||
[ | ||
['() ', [0, 0]], | ||
['=>', [0, 3], '#ff7b72'], | ||
[' ', [0, 5]], | ||
['true', [0, 6], '#79c0ff'], | ||
]; | ||
``` | ||
Locations are always `[line, column]`. | ||
### Transitioning code (for animations) | ||
@@ -100,5 +84,4 @@ | ||
The `transform` object will contain three token arrays: "create", "delete", and | ||
"retain". The `create` and `delete` arrays contains tuples with the token's | ||
text, location, and then color, when available. | ||
The `transform` object will contain an array of tokens, each of which with a | ||
provinance property of either "create", "delete", or "retain". | ||
@@ -112,35 +95,1 @@ ```tsx | ||
``` | ||
The `transform` variable is then | ||
```tsx | ||
{ | ||
"create": [["false", [0, 0], "#79c0ff"]], | ||
"delete": [["true", [0, 0], "#79c0ff"]], | ||
"retain": [], | ||
} | ||
``` | ||
The `retain` array contains tuples with the token's text, old position, new | ||
position, and color, when available. | ||
```tsx | ||
import { ready, transition, toString } from 'code-fns'; | ||
await ready(); | ||
const transform = transition(['tsx', '/*<t>*/true'], { t: '' }, { t: ' ' }); | ||
``` | ||
Here, the `transform` variable is | ||
```tsx | ||
{ | ||
"create": [[" ", [0, 0]]], | ||
"delete": [], | ||
"retain": [["true", [0, 0], [0, 4], "#79c0ff"]], | ||
} | ||
``` | ||
By interpolating between the old and new position, you may animate notes to | ||
their new location. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
11466982
32
1025
12
92