Comparing version 0.1.0 to 0.2.0
{ | ||
"name": "code-fns", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "A library for visualizing code.", | ||
@@ -24,3 +24,3 @@ "license": "MIT", | ||
"build": "tsc", | ||
"prepair": "tsc", | ||
"prepublish": "tsc", | ||
"preview": "vite preview", | ||
@@ -27,0 +27,0 @@ "prettier": "prettier --write .", |
133
README.md
@@ -10,1 +10,134 @@ # code-fns | ||
``` | ||
## Purpose | ||
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 | ||
is domain-agnostic, and will export tokens as plain objects to be converted to | ||
whatever format you choose. Specifically, code-fns was built for use in the | ||
Motion Canvas project, for visualizing code in videos and animations. Code-fns | ||
may also compute the transformation between different code blocks, so that you | ||
may animate between them. | ||
## Usage | ||
You must initialize the project with `ready`. | ||
```tsx | ||
import { ready } from 'code-fns'; | ||
await ready(); | ||
``` | ||
### Highlighting code | ||
Once initialized, you may highlight your code with | ||
```tsx | ||
import { ready, tokenColors } from 'code-fns'; | ||
await ready(); | ||
const tokens = tokenColors(['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) | ||
Code transitions use comment templating to adjust code. For instance, in any | ||
language with multiline comments using `/* */`, a tagged code string would look | ||
like | ||
```tsx | ||
(/*< params >*/) => {}; | ||
``` | ||
You may then replace these tags using `substitute`. | ||
```tsx | ||
import { ready, substitute, toString } from 'code-fns'; | ||
await ready(); | ||
const code = `(/*< params >*/) => { }`; | ||
const subbed = substitute(['tsx', code], { params: 'input: any' }); | ||
console.log(toString(subbed)); | ||
// (input: any) => { } | ||
``` | ||
With two substitutions, however, you may build a transition, which may serve as | ||
the basis for an animation. | ||
```tsx | ||
import { ready, transition, toString } from 'code-fns'; | ||
await ready(); | ||
const code = `(/*< params >*/) => { }`; | ||
const transform = transition( | ||
['tsx', code], | ||
{ params: 'input' }, | ||
{ params: 'other' }, | ||
); | ||
``` | ||
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. | ||
```tsx | ||
import { ready, transition, toString } from 'code-fns'; | ||
await ready(); | ||
const transform = transition(['tsx', '/*<t>*/'], { t: 'true' }, { t: 'false' }); | ||
``` | ||
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. |
import { describe, it, expect } from 'vitest'; | ||
import { parse, color, substitute, transition } from './code'; | ||
import { | ||
parse, | ||
tokenColors, | ||
substitute, | ||
transition, | ||
ready, | ||
toString, | ||
} from './code'; | ||
describe('code', () => { | ||
it('should stringify', async () => { | ||
await ready(); | ||
expect(toString(parse('tsx', 'true'))).toEqual('true'); | ||
}); | ||
it('should parse', async () => { | ||
expect(color(await parse('tsx', '() => true'))).toMatchInlineSnapshot(` | ||
await ready(); | ||
expect(parse('tsx', 'true')).toMatchInlineSnapshot(` | ||
{ | ||
"chars": [ | ||
{ | ||
"char": "t", | ||
"classList": [ | ||
"pl-c1", | ||
], | ||
"token": [ | ||
0, | ||
4, | ||
], | ||
}, | ||
{ | ||
"char": "r", | ||
"classList": [ | ||
"pl-c1", | ||
], | ||
"token": [ | ||
1, | ||
4, | ||
], | ||
}, | ||
{ | ||
"char": "u", | ||
"classList": [ | ||
"pl-c1", | ||
], | ||
"token": [ | ||
2, | ||
4, | ||
], | ||
}, | ||
{ | ||
"char": "e", | ||
"classList": [ | ||
"pl-c1", | ||
], | ||
"token": [ | ||
3, | ||
4, | ||
], | ||
}, | ||
], | ||
"language": "tsx", | ||
} | ||
`); | ||
}); | ||
it('should color tokens', async () => { | ||
await ready(); | ||
expect(tokenColors(['tsx', '() => true'])).toMatchInlineSnapshot(` | ||
[ | ||
@@ -43,4 +107,5 @@ [ | ||
it('should replace tags', async () => { | ||
expect(color(await substitute('tsx', '/*<t>*/', { t: 'true' }))).toEqual( | ||
color(await parse('tsx', 'true')), | ||
await ready(); | ||
expect(tokenColors(substitute(['tsx', '/*<t>*/'], { t: 'true' }))).toEqual( | ||
tokenColors(['tsx', 'true']), | ||
); | ||
@@ -50,9 +115,11 @@ }); | ||
it('should keep tags', async () => { | ||
expect(color(await substitute('tsx', '/*<t>*/', {}))).toEqual( | ||
color(await parse('tsx', '/*<t>*/')), | ||
await ready(); | ||
expect(tokenColors(substitute(['tsx', '/*<t>*/'], {}))).toEqual( | ||
tokenColors(['tsx', '/*<t>*/']), | ||
); | ||
}); | ||
it('should transform', async () => { | ||
expect(await transition('tsx', '/*<t>*/', { t: 'true' }, { t: 'false' })) | ||
it('should transition', async () => { | ||
await ready(); | ||
expect(transition(['tsx', '/*<t>*/'], { t: 'true' }, { t: 'false' })) | ||
.toMatchInlineSnapshot(` | ||
@@ -63,3 +130,2 @@ { | ||
"false", | ||
"#79c0ff", | ||
[ | ||
@@ -69,2 +135,3 @@ 0, | ||
], | ||
"#79c0ff", | ||
], | ||
@@ -75,3 +142,2 @@ ], | ||
"true", | ||
"#79c0ff", | ||
[ | ||
@@ -81,2 +147,3 @@ 0, | ||
], | ||
"#79c0ff", | ||
], | ||
@@ -89,10 +156,6 @@ ], | ||
it('should retain notes when substituting tag', async () => { | ||
const codez = `(/*<t>*/)=>{}`; | ||
const transformation = await transition( | ||
'tsx', | ||
codez, | ||
{ t: '' }, | ||
{ t: 't' }, | ||
); | ||
it('should retain nodes when substituting tag', async () => { | ||
await ready(); | ||
const codez = `(/*<t>*/)`; | ||
const transformation = transition(['tsx', codez], { t: '' }, { t: 't' }); | ||
expect(transformation).toMatchInlineSnapshot(` | ||
@@ -103,3 +166,2 @@ { | ||
"t", | ||
"#ffa657", | ||
[ | ||
@@ -109,2 +171,3 @@ 0, | ||
], | ||
"#c9d1d9", | ||
], | ||
@@ -116,3 +179,2 @@ ], | ||
"(", | ||
undefined, | ||
[ | ||
@@ -129,40 +191,58 @@ 0, | ||
")", | ||
undefined, | ||
[ | ||
0, | ||
2, | ||
], | ||
[ | ||
0, | ||
1, | ||
], | ||
], | ||
[ | ||
"=>", | ||
"#ff7b72", | ||
[ | ||
0, | ||
3, | ||
], | ||
[ | ||
0, | ||
2, | ||
], | ||
], | ||
[ | ||
"{}", | ||
undefined, | ||
], | ||
} | ||
`); | ||
}); | ||
}); | ||
describe('docs', () => { | ||
it('should print substitution', async () => { | ||
await ready(); | ||
const code = `(/*< params >*/) => { }`; | ||
const subbed = substitute(['tsx', code], { params: 'input: any' }); | ||
expect(toString(subbed)).toEqual('(input: any) => { }'); | ||
}); | ||
it('should move token', async () => { | ||
await ready(); | ||
expect(transition(['tsx', '/*<t>*/true'], { t: '' }, { t: ' ' })) | ||
.toMatchInlineSnapshot(` | ||
{ | ||
"create": [ | ||
[ | ||
0, | ||
5, | ||
" ", | ||
[ | ||
0, | ||
0, | ||
], | ||
], | ||
], | ||
"delete": [], | ||
"retain": [ | ||
[ | ||
0, | ||
4, | ||
"true", | ||
[ | ||
0, | ||
0, | ||
], | ||
[ | ||
0, | ||
4, | ||
], | ||
"#79c0ff", | ||
], | ||
], | ||
], | ||
} | ||
`); | ||
} | ||
`); | ||
}); | ||
}); |
169
src/code.ts
@@ -1,3 +0,3 @@ | ||
import { createStarryNight, all, Root } from '@wooorm/starry-night'; | ||
import type { RootContent } from 'hast'; | ||
import { createStarryNight, all } from '@wooorm/starry-night'; | ||
import type { Root, RootContent } from 'hast'; | ||
import style from './dark-style.json'; | ||
@@ -9,3 +9,23 @@ | ||
export function color(input: Char[]): [string, [number, number], string?][] { | ||
export type Parsable = [string, string] | { lang: string; code: string }; | ||
export interface Parsed<T extends Char> { | ||
language: string; | ||
chars: T[]; | ||
} | ||
function ensureParsed(input: Parsed<Char> | Parsable): Parsed<Char> { | ||
if (Array.isArray(input)) { | ||
return parse(input[0], input[1]); | ||
} else if ('code' in input) { | ||
return parse(input.lang, input.code); | ||
} else { | ||
return input as Parsed<Char>; | ||
} | ||
} | ||
export function tokenColors( | ||
code: Parsed<Char> | Parsable, | ||
): [string, [number, number], string?][] { | ||
const input = ensureParsed(code).chars; | ||
const result: [string, [number, number], string?][] = []; | ||
@@ -19,3 +39,3 @@ let lastColor = Symbol(); | ||
classList.length === 1 ? rules.get(`.${classList[0]}`) : new Map(); | ||
console.assert(styles?.size ?? 0 <= 1, `more styles than just color`); | ||
console.assert((styles?.size ?? 0) <= 1, `more styles than just color`); | ||
const color = styles?.get('color'); | ||
@@ -39,17 +59,36 @@ if (input[i].char === '\n') { | ||
let starryNight: { | ||
flagToScope: (s: string) => string | undefined; | ||
highlight: (c: string, s: string) => Root; | ||
} | null = null; | ||
const starryNightPromise = createStarryNight(all); | ||
starryNightPromise.then((sn) => (starryNight = sn)); | ||
export async function parse(language: string, code: string) { | ||
const starryNight = await starryNightPromise; | ||
export function ready() { | ||
return starryNightPromise; | ||
} | ||
export function toString(code: Parsed<Char> | Parsable): string { | ||
const parsed = ensureParsed(code); | ||
const result: string[] = []; | ||
parsed.chars.forEach(({ char }) => result.push(char)); | ||
return result.join(''); | ||
} | ||
export function parse(language: string, code: string): Parsed<Char> { | ||
if (starryNight == null) | ||
throw new Error('you must await ready() to initialize package'); | ||
const scope = starryNight.flagToScope(language); | ||
if (typeof scope !== 'string') | ||
if (typeof scope !== 'string') { | ||
throw new Error(`language ${language} not found`); | ||
} | ||
const parsed = starryNight.highlight(code, scope); | ||
// console.log(inspect(parsed, false, null, true)) | ||
const converted = recurse(parsed); | ||
// console.log(inspect(converted, false, null, true)) | ||
return converted; | ||
return { | ||
language, | ||
chars: converted, | ||
}; | ||
} | ||
interface Char { | ||
export interface Char { | ||
char: string; | ||
@@ -60,7 +99,7 @@ classList: string[]; | ||
interface RepChar extends Char { | ||
export interface RepChar extends Char { | ||
from: 'new' | 'old'; | ||
} | ||
interface FormChar extends Char { | ||
export interface FormChar extends Char { | ||
from: 'create' | 'keep' | 'delete'; | ||
@@ -116,8 +155,9 @@ } | ||
export async function substitute( | ||
language: string, | ||
code: string | Char[], | ||
export function substitute( | ||
code: Parsed<Char> | Parsable, | ||
subs: Record<string, string>, | ||
): Promise<RepChar[]> { | ||
const tree = Array.isArray(code) ? code : await parse(language, code); | ||
): Parsed<RepChar> { | ||
const parsed = ensureParsed(code); | ||
const language = parsed.language; | ||
const tree = parsed.chars; | ||
const replacements: [number, number][] = []; | ||
@@ -141,40 +181,43 @@ let final = ''; | ||
}); | ||
const parsed = await parse(language, final); | ||
const reparsed = parse(language, final); | ||
let [r, ri] = [0, 0]; | ||
let inReplacement = false; | ||
return parsed.map((char, at) => { | ||
if (inReplacement) { | ||
ri++; | ||
if (ri === replacements[r][1]) { | ||
inReplacement = false; | ||
r++; | ||
return { | ||
language, | ||
chars: reparsed.chars.map((char: Char, at: number) => { | ||
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; | ||
} | ||
} | ||
} else if (r < replacements.length) { | ||
const [rat] = replacements[r]; | ||
if (rat === at) { | ||
inReplacement = true; | ||
} | ||
} | ||
return { | ||
...char, | ||
from: inReplacement ? 'new' : 'old', | ||
}; | ||
}); | ||
return { | ||
...char, | ||
from: inReplacement ? 'new' : 'old', | ||
}; | ||
}), | ||
}; | ||
} | ||
export async function transform( | ||
language: string, | ||
tree: Char[], | ||
export function transform( | ||
code: Parsed<Char> | Parsable, | ||
start: Record<string, string>, | ||
final: Record<string, string>, | ||
): Promise<FormChar[]> { | ||
const before = await substitute(language, tree, start); | ||
const after = await substitute(language, tree, final); | ||
): FormChar[] { | ||
const tree = ensureParsed(code); | ||
const before = substitute(tree, start); | ||
const after = substitute(tree, final); | ||
let [bat] = [0]; | ||
let [aat] = [0]; | ||
const chars: FormChar[] = []; | ||
while (bat < before.length || aat < after.length) { | ||
const bchar = before[bat] ?? null; | ||
const achar = after[aat] ?? null; | ||
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') { | ||
@@ -206,15 +249,14 @@ chars.push({ | ||
export interface Transition { | ||
delete: [string, string, [number, number]][]; | ||
create: [string, string, [number, number]][]; | ||
retain: [string, string, [number, number], [number, number]][]; | ||
delete: [string, [number, number], string?][]; | ||
create: [string, [number, number], string?][]; | ||
retain: [string, [number, number], [number, number], string?][]; | ||
} | ||
export async function transition( | ||
language: string, | ||
code: string, | ||
export function transition( | ||
code: Parsed<Char> | Parsable, | ||
start: Record<string, string>, | ||
final: Record<string, string>, | ||
): Promise<Transition> { | ||
const tree = await parse(language, code); | ||
const chars = await transform(language, tree, start, final); | ||
): Transition { | ||
const tree = ensureParsed(code); | ||
const chars = transform(tree, start, final); | ||
const result: Transition = { | ||
@@ -267,9 +309,22 @@ delete: [], | ||
if (char.from === 'delete') { | ||
result.delete.push([char.char, color, [dln, dat]]); | ||
result.delete.push([ | ||
char.char, | ||
[dln, dat], | ||
...((color ? [color] : []) as [string?]), | ||
]); | ||
dat++; | ||
} else if (char.from === 'create') { | ||
result.create.push([char.char, color, [cln, cat]]); | ||
result.create.push([ | ||
char.char, | ||
[cln, cat], | ||
...((color ? [color] : []) as [string?]), | ||
]); | ||
cat++; | ||
} else if (char.from === 'keep') { | ||
result.retain.push([char.char, color, [cln, cat], [dln, dat]]); | ||
result.retain.push([ | ||
char.char, | ||
[dln, dat], | ||
[cln, cat], | ||
...((color ? [color] : []) as [string?]), | ||
]); | ||
dat++; | ||
@@ -276,0 +331,0 @@ cat++; |
@@ -10,3 +10,2 @@ { | ||
"strict": true, | ||
"sourceMap": true, | ||
"resolveJsonModule": true, | ||
@@ -13,0 +12,0 @@ "isolatedModules": true, |
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
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
47274
984
143