Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

code-fns

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

code-fns - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

4

package.json
{
"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 .",

@@ -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",
],
],
],
}
`);
}
`);
});
});

@@ -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,

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc