feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+286
View File
@@ -0,0 +1,286 @@
# shiki-magic-move
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]
Smoothly animated code blocks with Shiki. [Online Demo](https://shiki-magic-move.netlify.app/).
Shiki Magic Move is a low-level library for animating code blocks, and uses [Shiki](https://shiki.style/) as the syntax highlighter. You usually want to use it with a high-level integration like [Slidev](https://sli.dev/guide/syntax#shiki-magic-move).
At the core of the `shiki-magic-move` package is a framework-agnostic [core](./src/core.ts), and [renderer](./src/renderer.ts) — there are also framework wrappers for [Vue](./src/vue), [React](./src/react), and [Svelte](./src/svelte).
Each of the framework wrappers provides the following components:
- `ShikiMagicMove` - the main component to wrap the code block
- `ShikiMagicMovePrecompiled` - animations for compiled tokens, without the dependency on Shiki
- `ShikiMagicMoveRenderer` - the low-level renderer component
The `ShikiMagicMove` component requires you to provide a Shiki highlighter instance, and the styles are also required, and provided by `shiki-magic-move`. Whenever the `code` changes, the component will animate the changes.
## Installation
You're going to need Shiki Magic Move for animating the code blocks, and Shiki for syntax highlighting.
```bash
npm i shiki-magic-move shiki
```
## Usage
### Vue
Import `shiki-magic-move/vue`, and pass the highlighter instance to the `ShikiMagicMove` component.
```vue
<script setup>
import { getHighlighter } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/vue'
import { ref } from 'vue'
import 'shiki-magic-move/dist/style.css'
const highlighter = await getHighlighter({
themes: ['nord'],
langs: ['javascript', 'typescript'],
})
const code = ref(`const hello = 'world'`)
function animate() {
code.value = `let hi = 'hello'`
}
</script>
<template>
<ShikiMagicMove
lang="ts"
theme="nord"
:highlighter="highlighter"
:code="code"
:options="{ duration: 800, stagger: 0.3, lineNumbers: true }"
/>
<button @click="animate">
Animate
</button>
</template>
```
### React
Import `shiki-magic-move/react`, and pass the highlighter instance to the `ShikiMagicMove` component.
```tsx
import { useEffect, useState } from 'react'
import { getHighlighter, type HighlighterCore } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/react'
import 'shiki-magic-move/dist/style.css'
function App() {
const [code, setCode] = useState(`const hello = 'world'`)
const [highlighter, setHighlighter] = useState<HighlighterCore>()
useEffect(() => {
async function initializeHighlighter() {
const highlighter = await getHighlighter({
themes: ['nord'],
langs: ['javascript', 'typescript'],
})
setHighlighter(highlighter)
}
initializeHighlighter()
}, [])
function animate() {
setCode(`let hi = 'hello'`)
}
return (
<div>
{highlighter && (
<>
<ShikiMagicMove
lang="ts"
theme="nord"
highlighter={highlighter}
code={code}
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
/>
<button onClick={animate}>Animate</button>
</>
)}
</div>
)
}
```
### Solid
Import `shiki-magic-move/solid`, and pass the highlighter instance to the `ShikiMagicMove` component.
```tsx
import { getHighlighter, type HighlighterCore } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/solid'
import { createResource, createSignal } from 'solid-js'
import 'shiki-magic-move/dist/style.css'
function App() {
const [code, setCode] = createSignal(`const hello = 'world'`)
const [highlighter] = createResource(async () => {
const newHighlighter = await createHighlighter({
themes: Object.keys(bundledThemes),
langs: Object.keys(bundledLanguages),
})
return newHighlighter
})
function animate() {
setCode(`let hi = 'hello'`)
}
return (
<div>
<Show when={highlighter()}>
{highlighter => (
<>
<ShikiMagicMove
lang="ts"
theme="nord"
highlighter={highlighter()}
code={code()}
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
/>
<button onClick={animate}>Animate</button>
</>
)}
</Show>
</div>
)
}
```
### Svelte
Import `shiki-magic-move/svelte`, and pass the highlighter instance to the `ShikiMagicMove` component.
```svelte
<script lang='ts'>
import { getHighlighter } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/svelte'
import 'shiki-magic-move/dist/style.css'
const highlighter = getHighlighter({
themes: ['nord'],
langs: ['javascript', 'typescript'],
})
let code = $state(`const hello = 'world'`)
function animate() {
code = `let hi = 'hello'`
}
</script>
{#await highlighter then highlighter}
<ShikiMagicMove
lang='ts'
theme='nord'
{highlighter}
{code}
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
/>
<button onclick={animate}>Animate</button>
{/await}
```
### `ShikiMagicMovePrecompiled`
`ShikiMagicMovePrecompiled` is a lighter version of `ShikiMagicMove` that doesn't require Shiki. It's useful when you want to animate the compiled tokens directly. For example, in Vue:
```vue
<script setup>
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
import { ref } from 'vue'
const step = ref(1)
const compiledSteps = [/* Compiled token steps */]
</script>
<template>
<ShikiMagicMovePrecompiled
:steps="compiledSteps"
:step="step"
/>
<button @click="step++">
Next
</button>
</template>
```
To get the compiled tokens, you can run this somewhere else and serialize them into the component:
```ts
import { getHighlighter } from 'shiki'
import { codeToKeyedTokens, createMagicMoveMachine } from 'shiki-magic-move/core'
const shiki = await getHighlighter({
theme: 'nord',
langs: ['javascript', 'typescript'],
})
const codeSteps = [
`const hello = 'world'`,
`let hi = 'hello'`,
]
const machine = createMagicMoveMachine(
code => codeToKeyedTokens(shiki, code, {
lang: 'ts',
theme: 'nord',
}),
{
// options
}
)
const compiledSteps = codeSteps.map(code => machine.commit(code).current)
// Pass `compiledSteps` to the precompiled component
// If you do this on server-side or build-time, you can serialize `compiledSteps` into JSON
```
## How it works
You can read [The Magic In Shiki Magic Move](https://antfu.me/posts/shiki-magic-move) to understand how Shiki Magic Move works.
## Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
</a>
</p>
## License
[MIT](./LICENSE) License © 2023-PRESENT [Anthony Fu](https://github.com/antfu)
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/shiki-magic-move?style=flat&colorA=080f12&colorB=1fa669
[npm-version-href]: https://npmjs.com/package/shiki-magic-move
[npm-downloads-src]: https://img.shields.io/npm/dm/shiki-magic-move?style=flat&colorA=080f12&colorB=1fa669
[npm-downloads-href]: https://npmjs.com/package/shiki-magic-move
[bundle-src]: https://img.shields.io/bundlephobia/minzip/shiki-magic-move?style=flat&colorA=080f12&colorB=1fa669&label=minzip
[bundle-href]: https://bundlephobia.com/result?p=shiki-magic-move
[license-src]: https://img.shields.io/github/license/shikijs/shiki-magic-move.svg?style=flat&colorA=080f12&colorB=1fa669
[license-href]: https://github.com/shikijs/shiki-magic-move/blob/main/LICENSE
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
[jsdocs-href]: https://www.jsdocs.io/package/shiki-magic-move
+32
View File
@@ -0,0 +1,32 @@
import { HighlighterGeneric, ThemedToken } from 'shiki/core';
import { KeyedTokensInfo, MagicMoveDifferOptions, MatchedRanges } from './types.mjs';
export { KeyedToken, MagicMoveRenderOptions, Range } from './types.mjs';
import 'diff-match-patch-es';
type ArgumentsType<F extends Function> = F extends (...args: infer A) => any ? A : never;
declare function createMagicMoveMachine(codeToKeyedTokens: (code: string, lineNumbers?: boolean) => KeyedTokensInfo, options?: MagicMoveDifferOptions): {
readonly current: KeyedTokensInfo;
readonly previous: KeyedTokensInfo;
commit: (code: string, override?: MagicMoveDifferOptions) => {
current: KeyedTokensInfo;
previous: KeyedTokensInfo;
};
reset(): void;
};
declare function codeToKeyedTokens<BundledLangKeys extends string, BundledThemeKeys extends string>(highlighter: HighlighterGeneric<BundledLangKeys, BundledThemeKeys>, code: string, options: ArgumentsType<HighlighterGeneric<BundledLangKeys, BundledThemeKeys>['codeToTokens']>[1], lineNumbers?: boolean): KeyedTokensInfo;
declare function toKeyedTokens(code: string, tokens: ThemedToken[][], salt?: string, lineNumbers?: boolean): KeyedTokensInfo;
/**
* Run diff on two sets of tokens,
* and sync the keys from the first set to the second set if those tokens are matched
*/
declare function syncTokenKeys(from: KeyedTokensInfo, to: KeyedTokensInfo, options?: MagicMoveDifferOptions): {
from: KeyedTokensInfo;
to: KeyedTokensInfo;
};
/**
* Find ranges of text matches between two strings
* It uses `diff-match-patch` under the hood
*/
declare function findTextMatches(a: string, b: string, options?: MagicMoveDifferOptions): MatchedRanges[];
export { KeyedTokensInfo, MagicMoveDifferOptions, MatchedRanges, codeToKeyedTokens, createMagicMoveMachine, findTextMatches, syncTokenKeys, toKeyedTokens };
+32
View File
@@ -0,0 +1,32 @@
import { HighlighterGeneric, ThemedToken } from 'shiki/core';
import { KeyedTokensInfo, MagicMoveDifferOptions, MatchedRanges } from './types.js';
export { KeyedToken, MagicMoveRenderOptions, Range } from './types.js';
import 'diff-match-patch-es';
type ArgumentsType<F extends Function> = F extends (...args: infer A) => any ? A : never;
declare function createMagicMoveMachine(codeToKeyedTokens: (code: string, lineNumbers?: boolean) => KeyedTokensInfo, options?: MagicMoveDifferOptions): {
readonly current: KeyedTokensInfo;
readonly previous: KeyedTokensInfo;
commit: (code: string, override?: MagicMoveDifferOptions) => {
current: KeyedTokensInfo;
previous: KeyedTokensInfo;
};
reset(): void;
};
declare function codeToKeyedTokens<BundledLangKeys extends string, BundledThemeKeys extends string>(highlighter: HighlighterGeneric<BundledLangKeys, BundledThemeKeys>, code: string, options: ArgumentsType<HighlighterGeneric<BundledLangKeys, BundledThemeKeys>['codeToTokens']>[1], lineNumbers?: boolean): KeyedTokensInfo;
declare function toKeyedTokens(code: string, tokens: ThemedToken[][], salt?: string, lineNumbers?: boolean): KeyedTokensInfo;
/**
* Run diff on two sets of tokens,
* and sync the keys from the first set to the second set if those tokens are matched
*/
declare function syncTokenKeys(from: KeyedTokensInfo, to: KeyedTokensInfo, options?: MagicMoveDifferOptions): {
from: KeyedTokensInfo;
to: KeyedTokensInfo;
};
/**
* Find ranges of text matches between two strings
* It uses `diff-match-patch` under the hood
*/
declare function findTextMatches(a: string, b: string, options?: MagicMoveDifferOptions): MatchedRanges[];
export { KeyedTokensInfo, MagicMoveDifferOptions, MatchedRanges, codeToKeyedTokens, createMagicMoveMachine, findTextMatches, syncTokenKeys, toKeyedTokens };
+264
View File
@@ -0,0 +1,264 @@
import { diff } from 'diff-match-patch-es';
import { hash } from 'ohash';
// eslint-disable-next-line ts/no-unsafe-function-type
function createMagicMoveMachine(codeToKeyedTokens, options = {}) {
const EMPTY = toKeyedTokens('', []);
let previous = EMPTY;
let current = EMPTY;
function commit(code, override) {
previous = current;
const mergedOptions = override ? {
...options,
...override
} : options;
const newTokens = codeToKeyedTokens(code, mergedOptions.lineNumbers);
({
from: previous,
to: current
} = syncTokenKeys(previous, newTokens, mergedOptions));
return {
current,
previous
};
}
return {
get current() {
return current;
},
get previous() {
return previous;
},
commit,
reset() {
previous = EMPTY;
current = EMPTY;
}
};
}
function codeToKeyedTokens(highlighter, code, options, lineNumbers = false) {
const result = highlighter.codeToTokens(code, options);
return {
...toKeyedTokens(code, result.tokens,
// We put the lang and theme to participate in the hash calculation because they can affect the tokenization
JSON.stringify([options.lang, 'themes' in options ? options.themes : options.theme]), lineNumbers),
bg: result.bg,
fg: result.fg,
rootStyle: result.rootStyle,
themeName: result.themeName,
lang: options.lang
};
}
function toKeyedTokens(code, tokens, salt = '', lineNumbers = false) {
const hash$1 = hash(code + salt);
let lastOffset = 0;
let firstOffset = 0;
const lineNumberDigits = Math.ceil(Math.log10(tokens.length));
const keyed = splitWhitespaceTokens(tokens).flatMap((line, lineIdx) => {
firstOffset = line[0]?.offset || lastOffset;
const lastEl = line[line.length - 1];
if (!lastEl) lastOffset += 1;else lastOffset = lastEl.offset + lastEl.content.length;
const tokens = [...line, {
content: '\n',
offset: lastOffset
}];
if (lineNumbers) {
tokens.unshift({
key: `${hash$1}-ln-${lineIdx + 1}`,
content: `${String(lineIdx + 1).padStart(lineNumberDigits, ' ')} `,
offset: firstOffset,
htmlClass: 'shiki-magic-move-line-number'
});
}
return tokens;
}).map((token, idx) => {
const t = token;
t.key ||= `${hash$1}-${idx}`;
return t;
});
return {
code,
hash: hash$1,
tokens: keyed,
lineNumbers
};
}
function splitWhitespaceTokens(tokens) {
return tokens.map(line => {
return line.flatMap(token => {
if (token.content.match(/^\s+$/)) return token;
// eslint-disable-next-line regexp/no-super-linear-backtracking
const match = token.content.match(/^(\s*)(.*?)(\s*)$/);
if (!match) return token;
const [, leading, content, trailing] = match;
if (!leading && !trailing) return token;
const expanded = [{
...token,
offset: token.offset + leading.length,
content
}];
if (leading) {
expanded.unshift({
content: leading,
offset: token.offset
});
}
if (trailing) {
expanded.push({
content: trailing,
offset: token.offset + leading.length + content.length
});
}
return expanded;
});
});
}
/**
* Split a token into multiple tokens by given offsets.
*
* The offsets are relative to the token, and should be sorted.
*/
function splitToken(token, offsets) {
let lastOffset = 0;
const key = token.key;
let index = 0;
const tokens = [];
function getKey() {
if (index === 0) {
index++;
return key;
}
return `${key}-${index++}`;
}
for (const offset of offsets) {
if (offset > lastOffset) {
tokens.push({
...token,
content: token.content.slice(lastOffset, offset),
offset: token.offset + lastOffset,
key: getKey()
});
}
lastOffset = offset;
}
if (lastOffset < token.content.length) {
tokens.push({
...token,
content: token.content.slice(lastOffset),
offset: token.offset + lastOffset,
key: getKey()
});
}
return tokens;
}
/**
* Split 2D tokens array by given breakpoints.
*/
function splitTokens(tokens, breakpoints) {
const sorted = Array.from(breakpoints instanceof Set ? breakpoints : new Set(breakpoints)).sort((a, b) => a - b);
if (!sorted.length) return tokens;
return tokens.flatMap(token => {
const breakpointsInToken = sorted.filter(i => token.offset < i && i < token.offset + token.content.length).map(i => i - token.offset).sort((a, b) => a - b);
if (!breakpointsInToken.length) return token;
return splitToken(token, breakpointsInToken);
});
}
/**
* Run diff on two sets of tokens,
* and sync the keys from the first set to the second set if those tokens are matched
*/
function syncTokenKeys(from, to, options = {}) {
const {
splitTokens: shouldSplitTokens = false,
enhanceMatching = true
} = options;
// Run the diff and generate matches parts
// In the matched parts, we override the keys with the same key so that the transition group can know they are the same element
const matches = findTextMatches(from.code, to.code, options);
const tokensFrom = shouldSplitTokens ? splitTokens(from.tokens, matches.flatMap(m => m.from)) : from.tokens;
const tokensTo = shouldSplitTokens ? splitTokens(to.tokens, matches.flatMap(m => m.to)) : to.tokens;
const matchedKeys = new Set();
matches.forEach(match => {
const tokensF = tokensFrom.filter(t => t.offset >= match.from[0] && t.offset + t.content.length <= match.from[1]);
const tokensT = tokensTo.filter(t => t.offset >= match.to[0] && t.offset + t.content.length <= match.to[1]);
let idxF = 0;
let idxT = 0;
while (idxF < tokensF.length && idxT < tokensT.length) {
if (!tokensF[idxF] || !tokensT[idxT]) break;
if (tokensF[idxF].content === tokensT[idxT].content) {
// assign the key from the first set to the second set
tokensT[idxT].key = tokensF[idxF].key;
matchedKeys.add(tokensF[idxF].key);
idxF++;
idxT++;
} else if (tokensF[idxF + 1]?.content === tokensT[idxT].content) {
// console.log('Token missing match', tokensF[idxF], undefined)
idxF++;
} else if (tokensF[idxF].content === tokensT[idxT + 1]?.content) {
// console.log('Token missing match', undefined, tokensT[idxT])
idxT++;
} else {
// console.log('Token missing match', tokensF[idxF], tokensT[idxT])
idxF++;
idxT++;
}
}
});
if (enhanceMatching) {
for (const token of tokensFrom) {
if (matchedKeys.has(token.key)) continue;
if (token.content.length < 3 || !token.content.match(/^[\w-]+$/)) continue;
const matched = tokensTo.find(t => t.content === token.content && !matchedKeys.has(t.key));
if (matched) {
matched.key = token.key;
matchedKeys.add(token.key);
}
}
}
return {
from: tokensFrom.length === from.tokens.length ? from : {
...from,
tokens: tokensFrom
},
to: tokensTo.length === to.tokens.length ? to : {
...to,
tokens: tokensTo
}
};
}
/**
* Find ranges of text matches between two strings
* It uses `diff-match-patch` under the hood
*/
function findTextMatches(a, b, options = {}) {
let delta = diff(a, b);
delta = options.diffCleanup?.(delta) || delta;
let aContent = '';
let bContent = '';
const matched = [];
for (const [op, text] of delta) {
if (op === 0) {
matched.push({
from: [aContent.length, aContent.length + text.length],
to: [bContent.length, bContent.length + text.length],
content: text
});
aContent += text;
bContent += text;
} else if (op === -1) {
aContent += text;
} else if (op === 1) {
bContent += text;
}
}
if (aContent !== a || bContent !== b) throw new Error('Content mismatch');
return matched;
}
export { codeToKeyedTokens, createMagicMoveMachine, findTextMatches, syncTokenKeys, toKeyedTokens };
+2
View File
@@ -0,0 +1,2 @@
export { }
+2
View File
@@ -0,0 +1,2 @@
export { }
+3
View File
@@ -0,0 +1,3 @@
(() => {
throw new Error('`shiki-magic-move` does not have main entry point. Use `shiki-magic-move/vue` for Vue or `shiki-magic-move/core` for core logics.');
})();
+48
View File
@@ -0,0 +1,48 @@
import { HighlighterCore } from 'shiki/core';
import { MagicMoveRenderOptions, MagicMoveDifferOptions, KeyedTokensInfo } from './types.mjs';
import * as React from 'react';
import 'diff-match-patch-es';
interface ShikiMagicMoveProps {
highlighter: HighlighterCore;
lang: string;
theme: string;
code: string;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
className?: string;
tabindex?: number;
}
declare function ShikiMagicMove(props: ShikiMagicMoveProps): React.JSX.Element;
interface ShikiMagicMovePrecompiledProps {
steps: KeyedTokensInfo[];
step?: number;
animate?: boolean;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
}
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
declare function ShikiMagicMovePrecompiled({ steps, step, animate, options, onStart, onEnd, }: ShikiMagicMovePrecompiledProps): React.JSX.Element;
interface ShikiMagicMoveRendererProps {
animate?: boolean;
tokens: KeyedTokensInfo;
previous?: KeyedTokensInfo;
options?: MagicMoveRenderOptions;
onStart?: () => void;
onEnd?: () => void;
className?: string;
style?: React.CSSProperties;
}
/**
* A wrapper component to `MagicMoveRenderer`
*/
declare function ShikiMagicMoveRenderer({ animate, tokens, previous, options, onStart, onEnd, className, style, }: ShikiMagicMoveRendererProps): React.JSX.Element;
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+48
View File
@@ -0,0 +1,48 @@
import { HighlighterCore } from 'shiki/core';
import { MagicMoveRenderOptions, MagicMoveDifferOptions, KeyedTokensInfo } from './types.js';
import * as React from 'react';
import 'diff-match-patch-es';
interface ShikiMagicMoveProps {
highlighter: HighlighterCore;
lang: string;
theme: string;
code: string;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
className?: string;
tabindex?: number;
}
declare function ShikiMagicMove(props: ShikiMagicMoveProps): React.JSX.Element;
interface ShikiMagicMovePrecompiledProps {
steps: KeyedTokensInfo[];
step?: number;
animate?: boolean;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
}
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
declare function ShikiMagicMovePrecompiled({ steps, step, animate, options, onStart, onEnd, }: ShikiMagicMovePrecompiledProps): React.JSX.Element;
interface ShikiMagicMoveRendererProps {
animate?: boolean;
tokens: KeyedTokensInfo;
previous?: KeyedTokensInfo;
options?: MagicMoveRenderOptions;
onStart?: () => void;
onEnd?: () => void;
className?: string;
style?: React.CSSProperties;
}
/**
* A wrapper component to `MagicMoveRenderer`
*/
declare function ShikiMagicMoveRenderer({ animate, tokens, previous, options, onStart, onEnd, className, style, }: ShikiMagicMoveRendererProps): React.JSX.Element;
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+132
View File
@@ -0,0 +1,132 @@
import * as React from 'react';
import { codeToKeyedTokens, createMagicMoveMachine, syncTokenKeys, toKeyedTokens } from './core.mjs';
import { MagicMoveRenderer } from './renderer.mjs';
import 'diff-match-patch-es';
import 'ohash';
function normalizeCSSProperties(css) {
if (typeof css === 'string') {
const style = {};
css?.split(';').forEach(pair => {
const [key, value] = pair.split(':');
if (key && value) style[key.trim()] = value.trim();
});
return style;
}
return css;
}
/**
* A wrapper component to `MagicMoveRenderer`
*/
function ShikiMagicMoveRenderer({
animate = true,
tokens,
previous,
options,
onStart,
onEnd,
className,
style
}) {
const container = React.useRef(null);
const renderer = React.useRef(null);
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
if (!container.current) return;
// Remove previous content
container.current.innerHTML = '';
setIsMounted(true);
renderer.current = new MagicMoveRenderer(container.current);
}, []);
React.useEffect(() => {
async function render() {
if (!renderer.current) return;
Object.assign(renderer.current.options, options);
if (animate) {
if (previous) renderer.current.replace(previous);
onStart?.();
await renderer.current.render(tokens);
onEnd?.();
} else {
renderer.current.replace(tokens);
}
}
render();
// FIXME: we should only re-render when tokens change, but react-hooks rule doesn't allow.
// Try to correct the dependency array if something goes wrong.
}, [tokens]);
return /*#__PURE__*/React.createElement("pre", {
ref: container,
className: `shiki-magic-move-container ${className || ''}`.trim(),
style: style
}, /*#__PURE__*/React.createElement("div", null, isMounted ? undefined : tokens.tokens.map(token => {
if (token.content === '\n') return /*#__PURE__*/React.createElement("br", {
key: token.key
});
return /*#__PURE__*/React.createElement("span", {
style: {
...normalizeCSSProperties(token.htmlStyle),
color: token.color
},
className: ['shiki-magic-move-item', token.htmlClass].filter(Boolean).join(' '),
key: token.key
}, token.content);
})));
}
function ShikiMagicMove(props) {
const codeToTokens = React.useRef((code, lineNumbers) => codeToKeyedTokens(props.highlighter, code, {
lang: props.lang,
theme: props.theme
}, lineNumbers));
const machine = React.useRef(null);
machine.current = machine.current || createMagicMoveMachine((code, lineNumbers) => codeToTokens.current(code, lineNumbers));
const lineNumbers = props.options?.lineNumbers ?? false;
const result = React.useMemo(() => {
if (props.code === machine.current.current.code && props.theme === machine.current.current.themeName && props.lang === machine.current.current.lang && lineNumbers === machine.current.current.lineNumbers) {
return machine.current;
}
return machine.current.commit(props.code, props.options);
}, [props.code, props.options, props.theme, props.lang, lineNumbers]);
return /*#__PURE__*/React.createElement(ShikiMagicMoveRenderer, {
tokens: result.current,
options: props.options,
previous: result.previous,
onStart: props.onStart,
onEnd: props.onEnd,
className: props.className
});
}
const EMPTY = /* @__PURE__ */toKeyedTokens('', []);
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
function ShikiMagicMovePrecompiled({
steps,
step = 0,
animate = true,
options,
onStart,
onEnd
}) {
const [previous, setPrevious] = React.useState(EMPTY);
const result = React.useMemo(() => {
const res = syncTokenKeys(previous, steps[Math.min(step, steps.length - 1)], options);
setPrevious(res.to);
return res;
}, [previous, steps, step, options]);
return /*#__PURE__*/React.createElement(ShikiMagicMoveRenderer, {
tokens: result.to,
previous: result.from,
options: options,
animate: animate,
onStart: onStart,
onEnd: onEnd
});
}
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+31
View File
@@ -0,0 +1,31 @@
import { MagicMoveRenderOptions, KeyedTokensInfo } from './types.mjs';
import 'diff-match-patch-es';
import 'shiki/core';
declare const defaultOptions: Required<MagicMoveRenderOptions>;
declare class MagicMoveRenderer {
private mapDom;
private container;
private anchor;
private previousPromises;
options: Required<MagicMoveRenderOptions>;
private isFirstRender;
constructor(target: HTMLElement | string, options?: MagicMoveRenderOptions);
private applyElementContent;
private applyElementStyle;
private applyElement;
private applyNodeStyle;
private applyContainerStyle;
private registerTransitionEnd;
setCssVariables(): void;
/**
* Replace tokens without animation
*/
replace(step: KeyedTokensInfo): void;
/**
* Render tokens with animation
*/
render(step: KeyedTokensInfo): Promise<void>;
}
export { MagicMoveRenderer, defaultOptions };
+31
View File
@@ -0,0 +1,31 @@
import { MagicMoveRenderOptions, KeyedTokensInfo } from './types.js';
import 'diff-match-patch-es';
import 'shiki/core';
declare const defaultOptions: Required<MagicMoveRenderOptions>;
declare class MagicMoveRenderer {
private mapDom;
private container;
private anchor;
private previousPromises;
options: Required<MagicMoveRenderOptions>;
private isFirstRender;
constructor(target: HTMLElement | string, options?: MagicMoveRenderOptions);
private applyElementContent;
private applyElementStyle;
private applyElement;
private applyNodeStyle;
private applyContainerStyle;
private registerTransitionEnd;
setCssVariables(): void;
/**
* Replace tokens without animation
*/
replace(step: KeyedTokensInfo): void;
/**
* Render tokens with animation
*/
render(step: KeyedTokensInfo): Promise<void>;
}
export { MagicMoveRenderer, defaultOptions };
+313
View File
@@ -0,0 +1,313 @@
const CLASS_PREFIX = 'shiki-magic-move';
const CLASS_LEAVE_FROM = `${CLASS_PREFIX}-leave-from`;
const CLASS_LEAVE_TO = `${CLASS_PREFIX}-leave-to`;
const CLASS_LEAVE_ACTIVE = `${CLASS_PREFIX}-leave-active`;
const CLASS_ENTER_FROM = `${CLASS_PREFIX}-enter-from`;
const CLASS_ENTER_TO = `${CLASS_PREFIX}-enter-to`;
const CLASS_ENTER_ACTIVE = `${CLASS_PREFIX}-enter-active`;
const CLASS_MOVE = `${CLASS_PREFIX}-move`;
const CLASS_CONTAINER_RESIZE = `${CLASS_PREFIX}-container-resize`;
const CLASS_CONTAINER_RESTYLE = `${CLASS_PREFIX}-container-restyle`;
const defaultOptions = {
globalScale: 1,
duration: 500,
delayMove: 0.3,
delayLeave: 0.1,
delayEnter: 0.7,
delayContainer: 0.4,
stagger: 0,
easing: 'ease',
animateContainer: true,
containerStyle: true
};
class MagicMoveRenderer {
mapDom = new Map();
previousPromises = [];
isFirstRender = true;
constructor(target, options = {}) {
this.options = {
...defaultOptions,
...options
};
if (typeof target === 'string') this.container = document.querySelector(target);else this.container = target;
// Create anchor
this.anchor = document.createElement('span');
this.anchor.style.position = 'absolute';
this.anchor.style.top = '0';
this.anchor.style.left = '0';
this.anchor.style.height = '1px';
this.anchor.style.width = '1px';
this.container.prepend(this.anchor);
}
applyElementContent(el, token) {
if (token.content !== '\n') {
el.textContent = token.content;
el.classList.add(`${CLASS_PREFIX}-item`);
}
}
applyElementStyle(el, token) {
if (token.htmlStyle) {
if (typeof token.htmlStyle === 'string') {
el.setAttribute('style', token.htmlStyle);
} else {
for (const [key, value] of Object.entries(token.htmlStyle)) {
el.style.setProperty(key, value);
}
}
}
if (token.htmlClass) el.className = [`${CLASS_PREFIX}-item`, token.htmlClass].join(' ');
if (token.color) el.style.color = token.color;
if (token.bgColor) el.style.backgroundColor = token.bgColor;
}
applyElement(el, token) {
this.applyElementContent(el, token);
this.applyElementStyle(el, token);
}
applyNodeStyle(node, step) {
if (step.bg) node.style.backgroundColor = step.bg;
if (step.fg) node.style.color = step.fg;
if (step.rootStyle) {
const items = step.rootStyle.split(';');
for (const item of items) {
const [key, value] = item.split(':');
if (key && value) node.style.setProperty(key.trim(), value.trim());
}
}
}
applyContainerStyle(step) {
if (!this.options.containerStyle) return;
this.applyNodeStyle(this.container, step);
}
registerTransitionEnd(el, cb) {
return () => {
let resolved = false;
let resolve = () => {};
const promise = Promise.race([
// wait for the finish of all animation and transitions on this element then invoke the callback
Promise.allSettled(el.getAnimations().map(animation => animation.finished)).then(() => cb()),
// race it with another promise so it's still resolvable separately
new Promise(_resolve => {
resolve = () => {
if (resolved) return;
resolved = true;
cb();
_resolve();
};
})]);
promise.resolve = resolve;
return promise;
};
}
setCssVariables() {
// Update CSS variables
this.container.style.setProperty('--smm-duration', `${this.options.duration}ms`);
this.container.style.setProperty('--smm-delay-move', `${this.options.delayMove}`);
this.container.style.setProperty('--smm-delay-leave', `${this.options.delayLeave}`);
this.container.style.setProperty('--smm-delay-enter', `${this.options.delayEnter}`);
this.container.style.setProperty('--smm-delay-container', `${this.options.delayContainer}`);
this.container.style.setProperty('--smm-easing', this.options.easing);
this.container.style.setProperty('--smm-stagger', '0');
}
/**
* Replace tokens without animation
*/
replace(step) {
const newDomMap = new Map();
const newChildren = step.tokens.map(token => {
if (this.mapDom.has(token.key)) {
const el = this.mapDom.get(token.key);
this.applyElement(el, token);
newDomMap.set(token.key, el);
this.mapDom.delete(token.key);
return el;
} else {
const el = document.createElement(token.content === '\n' ? 'br' : 'span');
this.applyElement(el, token);
newDomMap.set(token.key, el);
return el;
}
});
this.container.replaceChildren(this.anchor, ...newChildren);
this.applyContainerStyle(step);
this.mapDom = newDomMap;
}
// Note: This function is intentionally not async to keep the operations sync
/**
* Render tokens with animation
*/
render(step) {
this.setCssVariables();
const newDomMap = new Map();
const move = [];
const enter = [];
const leave = [];
const promises = [];
this.previousPromises.forEach(p => p.resolve());
this.previousPromises = [];
// Callbacks to run after forced reflow
const postReflow = [];
const {
globalScale: scale
} = this.options;
// Record the current position of the elements (before rerendering)
const position = new Map();
let anchorRect = this.anchor.getBoundingClientRect();
const containerRect = this.container.getBoundingClientRect();
for (const el of this.mapDom.values()) {
const rect = el.getBoundingClientRect();
position.set(el, {
x: rect.x - anchorRect.x,
y: rect.y - anchorRect.y
});
}
const newChildren = step.tokens.map(token => {
if (this.mapDom.has(token.key)) {
const el = this.mapDom.get(token.key);
this.applyElementContent(el, token);
postReflow.push(() => {
// Update color and style after reflow
this.applyElementStyle(el, token);
});
move.push(el);
newDomMap.set(token.key, el);
this.mapDom.delete(token.key);
return el;
} else {
const el = document.createElement(token.content === '\n' ? 'br' : 'span');
this.applyElement(el, token);
enter.push(el);
newDomMap.set(token.key, el);
return el;
}
});
for (const [_, el] of this.mapDom) {
if (el.tagName === 'BR') continue;
leave.push(el);
}
for (const el of leave) el.style.position = 'absolute';
// Update DOM
this.container.replaceChildren(this.anchor, ...newChildren, ...leave);
this.mapDom = newDomMap;
// Lock leave elements to their position with absolute positioning
leave.forEach((el, idx) => {
el.style.position = 'absolute';
const pos = position.get(el);
el.style.top = `${pos.y / scale}px`;
el.style.left = `${pos.x / scale}px`;
if (this.options.stagger) el.style.setProperty('--smm-stagger', `${idx * this.options.stagger}ms`);else el.style.removeProperty('--smm-stagger');
el.classList.add(CLASS_LEAVE_FROM);
el.classList.add(CLASS_LEAVE_ACTIVE);
postReflow.unshift(() => {
el.classList.remove(CLASS_LEAVE_FROM);
el.classList.add(CLASS_LEAVE_TO);
});
promises.push(this.registerTransitionEnd(el, () => {
el.classList.remove(CLASS_LEAVE_FROM);
el.classList.remove(CLASS_LEAVE_ACTIVE);
el.classList.remove(CLASS_ENTER_TO);
el.remove();
}));
});
// Apply enter animation
if (!this.isFirstRender) {
enter.forEach((el, idx) => {
el.classList.add(CLASS_ENTER_FROM);
el.classList.add(CLASS_ENTER_ACTIVE);
if (this.options.stagger) el.style.setProperty('--smm-stagger', `${idx * this.options.stagger}ms`);else el.style.removeProperty('--smm-stagger');
postReflow.push(() => {
el.classList.remove(CLASS_ENTER_FROM);
el.classList.add(CLASS_ENTER_TO);
});
promises.push(this.registerTransitionEnd(el, () => {
el.classList.remove(CLASS_ENTER_FROM);
el.classList.remove(CLASS_ENTER_ACTIVE);
el.classList.remove(CLASS_ENTER_TO);
}));
});
}
// We recalculate the anchor position because the container might be moved
anchorRect = this.anchor.getBoundingClientRect();
// Set the position of the move elements to the old position
// Set the transition duration to 0ms to make it immediate
move.forEach((el, idx) => {
const newRect = el.getBoundingClientRect();
const newPos = {
x: newRect.x - anchorRect.x,
y: newRect.y - anchorRect.y
};
const oldPos = position.get(el);
el.style.transitionDuration = el.style.transitionDelay = '0ms';
const dx = (oldPos.x - newPos.x) / scale;
const dy = (oldPos.y - newPos.y) / scale;
el.style.transform = `translate(${dx}px, ${dy}px)`;
if (this.options.stagger) el.style.setProperty('--smm-stagger', `${idx * this.options.stagger}ms`);else el.style.removeProperty('--smm-stagger');
postReflow.unshift(() => {
// Remove transform overrides, so it will start animating back to the new position
el.classList.add(CLASS_MOVE);
el.style.transform = el.style.transitionDuration = el.style.transitionDelay = '';
});
promises.push(this.registerTransitionEnd(el, () => {
el.classList.remove(CLASS_MOVE);
}));
});
// Animate the container size
if (this.options.animateContainer && !this.isFirstRender) {
const newRect = this.container.getBoundingClientRect();
if (newRect.width !== containerRect.width || newRect.height !== containerRect.height) {
this.container.style.transitionDuration = this.container.style.transitionDelay = '0ms';
this.container.style.height = `${containerRect.height / scale}px`;
this.container.style.width = `${containerRect.width / scale}px`;
postReflow.unshift(() => {
this.container.classList.add(CLASS_CONTAINER_RESIZE);
this.container.style.transitionDuration = this.container.style.transitionDelay = '';
this.container.style.height = `${newRect.height / scale}px`;
this.container.style.width = `${newRect.width / scale}px`;
});
promises.push(this.registerTransitionEnd(this.container, () => {
this.container.classList.remove(CLASS_CONTAINER_RESIZE);
this.container.style.height = this.container.style.width = '';
}));
}
}
if (this.options.containerStyle) {
if (this.isFirstRender) {
this.applyContainerStyle(step);
} else {
postReflow.push(() => {
this.container.classList.add(CLASS_CONTAINER_RESTYLE);
this.applyContainerStyle(step);
});
promises.push(this.registerTransitionEnd(this.container, () => {
this.container.classList.remove(CLASS_CONTAINER_RESTYLE);
}));
}
}
// Trigger reflow to apply the transform
forceReflow();
postReflow.forEach(cb => cb());
// we need to call the functions that return the promises after the reflow
// to to allow getAnimations to have the correct transitions
const actualPromises = promises.map(promise => promise());
this.isFirstRender = false;
this.previousPromises = actualPromises;
return Promise.all(actualPromises).then();
}
}
// synchronously force layout to put elements into a certain state
function forceReflow() {
return document.body.offsetHeight;
}
export { MagicMoveRenderer, defaultOptions };
+51
View File
@@ -0,0 +1,51 @@
import * as solid_js from 'solid-js';
import { JSX } from 'solid-js';
import { HighlighterCore } from 'shiki/core';
import { MagicMoveRenderOptions, MagicMoveDifferOptions, KeyedTokensInfo } from './types.mjs';
import 'diff-match-patch-es';
interface ShikiMagicMoveProps {
highlighter: HighlighterCore;
lang: string;
theme: string;
code: string;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
class?: string;
tabindex?: number;
}
declare function ShikiMagicMove(props: ShikiMagicMoveProps): solid_js.JSX.Element;
interface ShikiMagicMovePrecompiledProps {
steps: KeyedTokensInfo[];
step?: number;
animate?: boolean;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
}
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
declare function ShikiMagicMovePrecompiled(props: ShikiMagicMovePrecompiledProps): solid_js.JSX.Element;
/** @jsxImportSource solid-js */
interface ShikiMagicMoveRendererProps {
animate?: boolean;
tokens: KeyedTokensInfo;
previous?: KeyedTokensInfo;
options?: MagicMoveRenderOptions;
onStart?: () => void;
onEnd?: () => void;
class?: string;
style?: JSX.CSSProperties;
}
/**
* A wrapper component to `MagicMoveRenderer`
*/
declare function ShikiMagicMoveRenderer(props: ShikiMagicMoveRendererProps): JSX.Element;
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+51
View File
@@ -0,0 +1,51 @@
import * as solid_js from 'solid-js';
import { JSX } from 'solid-js';
import { HighlighterCore } from 'shiki/core';
import { MagicMoveRenderOptions, MagicMoveDifferOptions, KeyedTokensInfo } from './types.js';
import 'diff-match-patch-es';
interface ShikiMagicMoveProps {
highlighter: HighlighterCore;
lang: string;
theme: string;
code: string;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
class?: string;
tabindex?: number;
}
declare function ShikiMagicMove(props: ShikiMagicMoveProps): solid_js.JSX.Element;
interface ShikiMagicMovePrecompiledProps {
steps: KeyedTokensInfo[];
step?: number;
animate?: boolean;
options?: MagicMoveRenderOptions & MagicMoveDifferOptions;
onStart?: () => void;
onEnd?: () => void;
}
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
declare function ShikiMagicMovePrecompiled(props: ShikiMagicMovePrecompiledProps): solid_js.JSX.Element;
/** @jsxImportSource solid-js */
interface ShikiMagicMoveRendererProps {
animate?: boolean;
tokens: KeyedTokensInfo;
previous?: KeyedTokensInfo;
options?: MagicMoveRenderOptions;
onStart?: () => void;
onEnd?: () => void;
class?: string;
style?: JSX.CSSProperties;
}
/**
* A wrapper component to `MagicMoveRenderer`
*/
declare function ShikiMagicMoveRenderer(props: ShikiMagicMoveRendererProps): JSX.Element;
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+165
View File
@@ -0,0 +1,165 @@
import { insert, memo, effect, style, className, template, use, createComponent } from 'solid-js/web';
import { createSignal, createEffect, createMemo } from 'solid-js';
import { createMagicMoveMachine, codeToKeyedTokens, syncTokenKeys, toKeyedTokens } from './core.mjs';
import { MagicMoveRenderer } from './renderer.mjs';
import 'diff-match-patch-es';
import 'ohash';
function normalizeCSSProperties(css) {
if (typeof css === 'string') {
const style = {};
css?.split(';').forEach(pair => {
const [key, value] = pair.split(':');
if (key && value) style[key.trim()] = value.trim();
});
return style;
}
return css;
}
var _tmpl$ = /*#__PURE__*/template(`<pre><div>`),
_tmpl$2 = /*#__PURE__*/template(`<br>`),
_tmpl$3 = /*#__PURE__*/template(`<span>`);
/**
* A wrapper component to `MagicMoveRenderer`
*/
function ShikiMagicMoveRenderer(props) {
let container;
let renderer;
const [isMounted, setIsMounted] = createSignal(false);
createEffect(() => {
if (!container) return;
// Remove previous content
container.innerHTML = '';
setIsMounted(true);
renderer = new MagicMoveRenderer(container);
});
createEffect(() => {
async function render() {
if (!renderer) return;
Object.assign(renderer.options, props.options);
if (props.animate === undefined || props.animate === true) {
if (props.previous) renderer.replace(props.previous);
props.onStart?.();
await renderer.render(props.tokens);
props.onEnd?.();
} else {
renderer.replace(props.tokens);
}
}
render();
});
return (() => {
var _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
var _ref$ = container;
typeof _ref$ === "function" ? use(_ref$, _el$) : container = _el$;
insert(_el$2, (() => {
var _c$ = memo(() => !!isMounted());
return () => _c$() ? undefined : props.tokens?.tokens.map(token => {
if (token.content === '\n') return _tmpl$2();
return (() => {
var _el$4 = _tmpl$3();
insert(_el$4, () => token.content);
effect(_p$ => {
var _v$3 = {
...normalizeCSSProperties(token.htmlStyle),
color: token.color
},
_v$4 = ['shiki-magic-move-item', token.htmlClass].filter(Boolean).join(' ');
_p$.e = style(_el$4, _v$3, _p$.e);
_v$4 !== _p$.t && className(_el$4, _p$.t = _v$4);
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$4;
})();
});
})());
effect(_p$ => {
var _v$ = `shiki-magic-move-container ${props.class || ''}`.trim(),
_v$2 = props.style;
_v$ !== _p$.e && className(_el$, _p$.e = _v$);
_p$.t = style(_el$, _v$2, _p$.t);
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$;
})();
}
function ShikiMagicMove(props) {
const codeToTokens = (code, lineNumbers) => codeToKeyedTokens(props.highlighter, code, {
lang: props.lang,
theme: props.theme
}, lineNumbers);
const machine = createMagicMoveMachine((code, lineNumbers) => codeToTokens(code, lineNumbers));
const result = createMemo(() => {
const lineNumbers = props.options?.lineNumbers ?? false;
if (props.code === machine.current.code && props.theme === machine.current.themeName && props.lang === machine.current.lang && lineNumbers === machine.current.lineNumbers) {
return machine;
}
return machine.commit(props.code, props.options);
});
return createComponent(ShikiMagicMoveRenderer, {
get tokens() {
return result().current;
},
get options() {
return props.options;
},
get previous() {
return result().previous;
},
get onStart() {
return props.onStart;
},
get onEnd() {
return props.onEnd;
},
get ["class"]() {
return props.class;
}
});
}
const EMPTY = /* @__PURE__ */toKeyedTokens('', []);
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
function ShikiMagicMovePrecompiled(props) {
const [previous, setPrevious] = createSignal(EMPTY);
const result = createMemo(() => {
const res = syncTokenKeys(previous(), props.steps[Math.min(props.step ?? 0, props.steps.length - 1)], props.options);
setPrevious(res.to);
return res;
});
return createComponent(ShikiMagicMoveRenderer, {
get tokens() {
return result().to;
},
get previous() {
return result().from;
},
get options() {
return props.options;
},
get animate() {
return props.animate ?? true;
},
get onStart() {
return props.onStart;
},
get onEnd() {
return props.onEnd;
}
});
}
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+52
View File
@@ -0,0 +1,52 @@
.shiki-magic-move-container {
position: relative;
white-space: pre;
}
.shiki-magic-move-line-number {
opacity: 0.3;
user-select: none;
}
.shiki-magic-move-item {
display: inline-block;
transition: color var(--smm-duration, 0.5s) var(--smm-easing, 'ease');
}
.shiki-magic-move-move {
transition: all var(--smm-duration, 0.5s) var(--smm-easing, 'ease');
}
.shiki-magic-move-enter-active,
.shiki-magic-move-leave-active {
transition: all var(--smm-duration, 0.5s) var(--smm-easing, 'ease');
}
.shiki-magic-move-container-resize,
.shiki-magic-move-container-restyle {
transition: all var(--smm-duration, 0.5s) var(--smm-easing, 'ease');
transition-delay: calc(var(--smm-duration, 0.5s) * var(--smm-delay-container, 1));
}
.shiki-magic-move-move {
transition-delay: calc(calc(var(--smm-duration, 0.5s) * var(--smm-delay-move, 1)) + var(--smm-stagger, 0));
z-index: 1;
}
.shiki-magic-move-enter-active {
transition-delay: calc(calc(var(--smm-duration, 0.5s) * var(--smm-delay-enter, 1)) + var(--smm-stagger, 0));
z-index: 1;
}
.shiki-magic-move-leave-active {
transition-delay: calc(calc(var(--smm-duration, 0.5s) * var(--smm-delay-leave, 1)) + var(--smm-stagger, 0));
}
.shiki-magic-move-enter-from,
.shiki-magic-move-leave-to {
opacity: 0;
}
br.shiki-magic-move-leave-active {
display: none;
}
+1
View File
@@ -0,0 +1 @@
export * from "./svelte/index.mjs"
+1
View File
@@ -0,0 +1 @@
export * from "./svelte/index.mjs"
+1
View File
@@ -0,0 +1 @@
export * from "./svelte/index.mjs"
@@ -0,0 +1,34 @@
<script lang='ts'>
import type { MagicMoveDifferOptions, MagicMoveRenderOptions } from 'shiki-magic-move/types'
import type { HighlighterCore } from 'shiki/core'
import { codeToKeyedTokens, createMagicMoveMachine } from 'shiki-magic-move/core'
import ShikiMagicMoveRenderer from './ShikiMagicMoveRenderer.svelte'
interface ShikiMagicMoveProps {
highlighter: HighlighterCore
lang: string
theme: string
code: string
options?: MagicMoveRenderOptions & MagicMoveDifferOptions
onStart?: () => void
onEnd?: () => void
class?: string
tabindex?: number
}
const { highlighter, lang, theme, code, options, onStart, onEnd, ...props }: ShikiMagicMoveProps = $props()
const machine = createMagicMoveMachine(
code => codeToKeyedTokens(highlighter, code, { lang, theme }, options?.lineNumbers),
options,
)
const result = $derived(machine.commit(code))
</script>
<ShikiMagicMoveRenderer
tokens={result.current}
previous={result.previous}
{options}
{onStart}
{onEnd}
{...props}
/>
@@ -0,0 +1,46 @@
<script lang='ts'>
import type { KeyedTokensInfo, MagicMoveDifferOptions, MagicMoveRenderOptions } from 'shiki-magic-move/types'
import { syncTokenKeys, toKeyedTokens } from 'shiki-magic-move/core'
import ShikiMagicMoveRenderer from './ShikiMagicMoveRenderer.svelte'
interface ShikiMagicMovePrecompileProps {
steps: KeyedTokensInfo[]
step?: number
animate?: boolean
options?: MagicMoveRenderOptions & MagicMoveDifferOptions
onStart?: () => void
onEnd?: () => void
class?: string
}
const {
steps,
step = 0,
animate = true,
options,
onStart,
onEnd,
}: ShikiMagicMovePrecompileProps = $props()
const EMPTY = toKeyedTokens('', [])
let previous = EMPTY
const result = $derived.by(() => {
if (steps.length === 0)
return
const res = syncTokenKeys(previous, steps[Math.min(step, steps.length - 1)], options)
previous = res.to
return res
})
</script>
{#if result}
<ShikiMagicMoveRenderer
tokens={result.to}
previous={result.from}
{options}
{animate}
{onStart}
{onEnd}
/>
{/if}
@@ -0,0 +1,72 @@
<script lang='ts'>
import type { KeyedTokensInfo, MagicMoveRenderOptions } from 'shiki-magic-move/types'
import { MagicMoveRenderer } from 'shiki-magic-move/renderer'
interface ShikiMagicMoveRendererProps {
class?: string
animate?: boolean
tokens: KeyedTokensInfo
previous?: KeyedTokensInfo
options?: MagicMoveRenderOptions
onStart?: () => void
onEnd?: () => void
}
const {
animate = true,
tokens,
previous,
options,
onStart,
onEnd,
...props
}: ShikiMagicMoveRendererProps = $props()
let container: HTMLPreElement
let renderer: MagicMoveRenderer
let isMounted = $state(false)
$effect(() => {
if (!container)
return
container.innerHTML = ''
isMounted = true
renderer = new MagicMoveRenderer(container)
})
$effect(() => {
async function render() {
if (!renderer)
return
Object.assign(renderer.options, options)
if (animate) {
if (previous)
renderer.replace(previous)
onStart?.()
await renderer.render(tokens)
onEnd?.()
}
else {
renderer.replace(tokens)
}
}
render()
})
</script>
<pre bind:this={container} class='shiki-magic-move-container {props?.class}' {...props}>
<!-- render initial tokens for SSR -->
{#if !isMounted}
{#each tokens.tokens as token (token.key)}
{#if token.content === '\n'}
<br />
{/if}
<span
class='shiki-magic-move-item'
style:color={token.color}
style={token.htmlStyle}>
{token.content}
</span>
{/each}
{/if}
</pre>
+4
View File
@@ -0,0 +1,4 @@
import ShikiMagicMove from './ShikiMagicMove.svelte';
import ShikiMagicMovePrecompiled from './ShikiMagicMovePrecompiled.svelte';
import ShikiMagicMoveRenderer from './ShikiMagicMoveRenderer.svelte';
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+4
View File
@@ -0,0 +1,4 @@
import ShikiMagicMove from './ShikiMagicMove.svelte';
import ShikiMagicMovePrecompiled from './ShikiMagicMovePrecompiled.svelte';
import ShikiMagicMoveRenderer from './ShikiMagicMoveRenderer.svelte';
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+4
View File
@@ -0,0 +1,4 @@
import ShikiMagicMove from "./ShikiMagicMove.svelte";
import ShikiMagicMovePrecompiled from "./ShikiMagicMovePrecompiled.svelte";
import ShikiMagicMoveRenderer from "./ShikiMagicMoveRenderer.svelte";
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+105
View File
@@ -0,0 +1,105 @@
import { Diff } from 'diff-match-patch-es';
import { ThemedToken, TokensResult } from 'shiki/core';
type Range = [number, number];
interface MatchedRanges {
from: Range;
to: Range;
content: string;
}
interface KeyedToken extends ThemedToken {
key: string;
htmlClass?: string;
}
interface KeyedTokensInfo extends Pick<TokensResult, 'bg' | 'fg' | 'rootStyle' | 'themeName'> {
code: string;
hash: string;
tokens: KeyedToken[];
lang?: string;
lineNumbers: boolean;
}
interface MagicMoveRenderOptions {
/**
* Duration of the animation in milliseconds
*
* @default 500
*/
duration?: number;
/**
* Ratio of the duration to delay the move animation
*
* @default 0.3
*/
delayMove?: number;
/**
* Ratio of the duration to delay the leave animation
*
* @default 0
*/
delayLeave?: number;
/**
* Ratio of the duration to delay the enter animation
*
* @default 0.7
*/
delayEnter?: number;
/**
* Ratio of the duration to delay the container animation
*
* @default 0.4
*/
delayContainer?: number;
/**
* Easing function
*
* @default 'ease'
*/
easing?: string;
/**
* Scale applies to the container or it's parents.
* Used to calculate the correct positions of the elements.
*
* @default 1
*/
globalScale?: number;
/**
* Apply animation to the container
*/
animateContainer?: boolean;
/**
* Apply styles (color and background) to the container
*
* @default true
*/
containerStyle?: boolean;
/**
* Stagger the animation of the tokens, in milliseconds
*
* @default 0
*/
stagger?: number;
}
interface MagicMoveDifferOptions {
/**
* Show line numbers
*
* @default false
*/
lineNumbers?: boolean;
/**
* Split the tokens at the edge of diff chunks
*
* @default false
*/
splitTokens?: boolean;
/**
* Algorithm to use to cleanup the diff
*/
diffCleanup?: ((diffs: Diff[]) => Diff[] | void);
/**
* Enhance the matching algorithm to match tokens that has same content
*/
enhanceMatching?: boolean;
}
export type { KeyedToken, KeyedTokensInfo, MagicMoveDifferOptions, MagicMoveRenderOptions, MatchedRanges, Range };
+105
View File
@@ -0,0 +1,105 @@
import { Diff } from 'diff-match-patch-es';
import { ThemedToken, TokensResult } from 'shiki/core';
type Range = [number, number];
interface MatchedRanges {
from: Range;
to: Range;
content: string;
}
interface KeyedToken extends ThemedToken {
key: string;
htmlClass?: string;
}
interface KeyedTokensInfo extends Pick<TokensResult, 'bg' | 'fg' | 'rootStyle' | 'themeName'> {
code: string;
hash: string;
tokens: KeyedToken[];
lang?: string;
lineNumbers: boolean;
}
interface MagicMoveRenderOptions {
/**
* Duration of the animation in milliseconds
*
* @default 500
*/
duration?: number;
/**
* Ratio of the duration to delay the move animation
*
* @default 0.3
*/
delayMove?: number;
/**
* Ratio of the duration to delay the leave animation
*
* @default 0
*/
delayLeave?: number;
/**
* Ratio of the duration to delay the enter animation
*
* @default 0.7
*/
delayEnter?: number;
/**
* Ratio of the duration to delay the container animation
*
* @default 0.4
*/
delayContainer?: number;
/**
* Easing function
*
* @default 'ease'
*/
easing?: string;
/**
* Scale applies to the container or it's parents.
* Used to calculate the correct positions of the elements.
*
* @default 1
*/
globalScale?: number;
/**
* Apply animation to the container
*/
animateContainer?: boolean;
/**
* Apply styles (color and background) to the container
*
* @default true
*/
containerStyle?: boolean;
/**
* Stagger the animation of the tokens, in milliseconds
*
* @default 0
*/
stagger?: number;
}
interface MagicMoveDifferOptions {
/**
* Show line numbers
*
* @default false
*/
lineNumbers?: boolean;
/**
* Split the tokens at the edge of diff chunks
*
* @default false
*/
splitTokens?: boolean;
/**
* Algorithm to use to cleanup the diff
*/
diffCleanup?: ((diffs: Diff[]) => Diff[] | void);
/**
* Enhance the matching algorithm to match tokens that has same content
*/
enhanceMatching?: boolean;
}
export type { KeyedToken, KeyedTokensInfo, MagicMoveDifferOptions, MagicMoveRenderOptions, MatchedRanges, Range };
+1
View File
@@ -0,0 +1 @@
+153
View File
@@ -0,0 +1,153 @@
import * as vue from 'vue';
import { PropType, App } from 'vue';
import { HighlighterCore } from 'shiki/core';
import { MagicMoveRenderOptions, MagicMoveDifferOptions, KeyedTokensInfo } from './types.mjs';
import 'diff-match-patch-es';
declare const ShikiMagicMove: vue.DefineComponent<vue.ExtractPropTypes<{
highlighter: {
type: PropType<HighlighterCore>;
required: true;
};
lang: {
type: StringConstructor;
required: true;
};
theme: {
type: StringConstructor;
required: true;
};
code: {
type: StringConstructor;
required: true;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, ("end" | "start")[], "end" | "start", vue.PublicProps, Readonly<vue.ExtractPropTypes<{
highlighter: {
type: PropType<HighlighterCore>;
required: true;
};
lang: {
type: StringConstructor;
required: true;
};
theme: {
type: StringConstructor;
required: true;
};
code: {
type: StringConstructor;
required: true;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>> & Readonly<{
onEnd?: ((...args: any[]) => any) | undefined;
onStart?: ((...args: any[]) => any) | undefined;
}>, {
options: MagicMoveRenderOptions & MagicMoveDifferOptions;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
declare const ShikiMagicMovePrecompiled: vue.DefineComponent<vue.ExtractPropTypes<{
steps: {
type: PropType<KeyedTokensInfo[]>;
required: true;
};
step: {
type: NumberConstructor;
default: number;
};
animate: {
type: BooleanConstructor;
default: boolean;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, ("end" | "start")[], "end" | "start", vue.PublicProps, Readonly<vue.ExtractPropTypes<{
steps: {
type: PropType<KeyedTokensInfo[]>;
required: true;
};
step: {
type: NumberConstructor;
default: number;
};
animate: {
type: BooleanConstructor;
default: boolean;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>> & Readonly<{
onEnd?: ((...args: any[]) => any) | undefined;
onStart?: ((...args: any[]) => any) | undefined;
}>, {
animate: boolean;
options: MagicMoveRenderOptions & MagicMoveDifferOptions;
step: number;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
/**
* A wrapper component to `MagicMoveRenderer`
*/
declare const ShikiMagicMoveRenderer: vue.DefineComponent<vue.ExtractPropTypes<{
animate: {
type: BooleanConstructor;
default: boolean;
};
tokens: {
type: PropType<KeyedTokensInfo>;
required: true;
};
previous: {
type: PropType<KeyedTokensInfo>;
required: false;
};
options: {
type: PropType<MagicMoveRenderOptions>;
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, ("end" | "start")[], "end" | "start", vue.PublicProps, Readonly<vue.ExtractPropTypes<{
animate: {
type: BooleanConstructor;
default: boolean;
};
tokens: {
type: PropType<KeyedTokensInfo>;
required: true;
};
previous: {
type: PropType<KeyedTokensInfo>;
required: false;
};
options: {
type: PropType<MagicMoveRenderOptions>;
};
}>> & Readonly<{
onEnd?: ((...args: any[]) => any) | undefined;
onStart?: ((...args: any[]) => any) | undefined;
}>, {
animate: boolean;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
declare function install(app: App<any>): void;
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer, install };
+153
View File
@@ -0,0 +1,153 @@
import * as vue from 'vue';
import { PropType, App } from 'vue';
import { HighlighterCore } from 'shiki/core';
import { MagicMoveRenderOptions, MagicMoveDifferOptions, KeyedTokensInfo } from './types.js';
import 'diff-match-patch-es';
declare const ShikiMagicMove: vue.DefineComponent<vue.ExtractPropTypes<{
highlighter: {
type: PropType<HighlighterCore>;
required: true;
};
lang: {
type: StringConstructor;
required: true;
};
theme: {
type: StringConstructor;
required: true;
};
code: {
type: StringConstructor;
required: true;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, ("end" | "start")[], "end" | "start", vue.PublicProps, Readonly<vue.ExtractPropTypes<{
highlighter: {
type: PropType<HighlighterCore>;
required: true;
};
lang: {
type: StringConstructor;
required: true;
};
theme: {
type: StringConstructor;
required: true;
};
code: {
type: StringConstructor;
required: true;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>> & Readonly<{
onEnd?: ((...args: any[]) => any) | undefined;
onStart?: ((...args: any[]) => any) | undefined;
}>, {
options: MagicMoveRenderOptions & MagicMoveDifferOptions;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
declare const ShikiMagicMovePrecompiled: vue.DefineComponent<vue.ExtractPropTypes<{
steps: {
type: PropType<KeyedTokensInfo[]>;
required: true;
};
step: {
type: NumberConstructor;
default: number;
};
animate: {
type: BooleanConstructor;
default: boolean;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, ("end" | "start")[], "end" | "start", vue.PublicProps, Readonly<vue.ExtractPropTypes<{
steps: {
type: PropType<KeyedTokensInfo[]>;
required: true;
};
step: {
type: NumberConstructor;
default: number;
};
animate: {
type: BooleanConstructor;
default: boolean;
};
options: {
type: PropType<MagicMoveRenderOptions & MagicMoveDifferOptions>;
default: () => {};
};
}>> & Readonly<{
onEnd?: ((...args: any[]) => any) | undefined;
onStart?: ((...args: any[]) => any) | undefined;
}>, {
animate: boolean;
options: MagicMoveRenderOptions & MagicMoveDifferOptions;
step: number;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
/**
* A wrapper component to `MagicMoveRenderer`
*/
declare const ShikiMagicMoveRenderer: vue.DefineComponent<vue.ExtractPropTypes<{
animate: {
type: BooleanConstructor;
default: boolean;
};
tokens: {
type: PropType<KeyedTokensInfo>;
required: true;
};
previous: {
type: PropType<KeyedTokensInfo>;
required: false;
};
options: {
type: PropType<MagicMoveRenderOptions>;
};
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, ("end" | "start")[], "end" | "start", vue.PublicProps, Readonly<vue.ExtractPropTypes<{
animate: {
type: BooleanConstructor;
default: boolean;
};
tokens: {
type: PropType<KeyedTokensInfo>;
required: true;
};
previous: {
type: PropType<KeyedTokensInfo>;
required: false;
};
options: {
type: PropType<MagicMoveRenderOptions>;
};
}>> & Readonly<{
onEnd?: ((...args: any[]) => any) | undefined;
onStart?: ((...args: any[]) => any) | undefined;
}>, {
animate: boolean;
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
declare function install(app: App<any>): void;
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer, install };
+172
View File
@@ -0,0 +1,172 @@
import { defineComponent, ref, onMounted, watch, nextTick, h, renderList, computed } from 'vue';
import { createMagicMoveMachine, codeToKeyedTokens, toKeyedTokens, syncTokenKeys } from './core.mjs';
import { MagicMoveRenderer } from './renderer.mjs';
import 'diff-match-patch-es';
import 'ohash';
/**
* A wrapper component to `MagicMoveRenderer`
*/
const ShikiMagicMoveRenderer = /* #__PURE__ */defineComponent({
name: 'ShikiMagicMoveRenderer',
props: {
animate: {
type: Boolean,
default: true
},
tokens: {
type: Object,
required: true
},
previous: {
type: Object,
required: false
},
options: {
type: Object
}
},
emits: ['end', 'start'],
setup(props, {
emit
}) {
const container = ref();
let isMounted = false;
onMounted(() => {
// Remove previous content
container.value.innerHTML = '';
isMounted = true;
const renderer = new MagicMoveRenderer(container.value);
watch(() => props.tokens, async tokens => {
Object.assign(renderer.options, props.options);
if (props.animate) {
if (props.previous) renderer.replace(props.previous);
await nextTick();
const process = renderer.render(tokens);
await nextTick();
emit('start');
await process;
emit('end');
} else {
renderer.replace(tokens);
}
}, {
immediate: true
});
});
return () => h('pre', {
ref: container,
class: 'shiki-magic-move-container'
},
// Render initial tokens for SSR
isMounted ? undefined : renderList(props.tokens.tokens, token => {
if (token.content === '\n') return h('br', {
key: token.key
});
return h('span', {
style: [{
color: token.color
}, token.htmlStyle],
class: ['shiki-magic-move-item', token.htmlClass],
key: token.key
}, token.content);
}));
}
});
const ShikiMagicMove = /* #__PURE__ */defineComponent({
name: 'ShikiMagicMove',
props: {
highlighter: {
type: Object,
required: true
},
lang: {
type: String,
required: true
},
theme: {
type: String,
required: true
},
code: {
type: String,
required: true
},
options: {
type: Object,
default: () => ({})
}
},
emits: ['start', 'end'],
setup(props, {
emit
}) {
const machine = createMagicMoveMachine(code => codeToKeyedTokens(props.highlighter, code, {
lang: props.lang,
theme: props.theme
}, props.options.lineNumbers), props.options);
const result = computed(() => machine.commit(props.code));
return () => h(ShikiMagicMoveRenderer, {
tokens: result.value.current,
options: props.options,
previous: result.value.previous,
onStart: () => emit('start'),
onEnd: () => emit('end')
});
}
});
/**
* Component to render a compiled magic move step,
* Where the tokens can be generated on build time.
*/
const ShikiMagicMovePrecompiled = /* #__PURE__ */defineComponent({
name: 'ShikiMagicMovePrecompiled',
props: {
steps: {
type: Array,
required: true
},
step: {
type: Number,
default: 0
},
animate: {
type: Boolean,
default: true
},
options: {
type: Object,
default: () => ({})
}
},
emits: ['start', 'end'],
setup(props, {
emit
}) {
const EMPTY = toKeyedTokens('', []);
let previous = EMPTY;
const result = computed(() => {
const res = syncTokenKeys(previous, props.steps[Math.min(props.step, props.steps.length - 1)], props.options);
previous = res.to;
return res;
});
return () => h(ShikiMagicMoveRenderer, {
tokens: result.value.to,
previous: result.value.from,
options: props.options,
animate: props.animate,
onStart: () => emit('start'),
onEnd: () => emit('end')
});
}
});
function install(app) {
app.component('ShikiMagicMove', ShikiMagicMove);
app.component('ShikiMagicMovePrecompiled', ShikiMagicMovePrecompiled);
app.component('ShikiMagicMoveRenderer', ShikiMagicMoveRenderer);
}
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer, install };
+99
View File
@@ -0,0 +1,99 @@
import { HighlighterCore } from 'shiki/core';
import { KeyedTokensInfo, MagicMoveRenderOptions, MagicMoveDifferOptions } from './types.mjs';
import 'diff-match-patch-es';
declare class ShikiMagicMoveRenderer extends HTMLElement {
private _animated;
private _tokens;
private _previous?;
private _options?;
private _class;
get animated(): boolean;
set animated(value: boolean);
get tokens(): KeyedTokensInfo;
set tokens(value: KeyedTokensInfo);
get previous(): KeyedTokensInfo | undefined;
set previous(value: KeyedTokensInfo);
get options(): MagicMoveRenderOptions | undefined;
set options(value: MagicMoveRenderOptions | undefined);
get class(): string;
set class(value: string);
private container?;
private renderer?;
private batchUpdate;
private hasUpdated;
constructor();
connectedCallback(): void;
propertyChangedCallback(): void;
transform(): Promise<void>;
}
declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move-renderer': ShikiMagicMoveRenderer;
}
}
declare class ShikiMagicMove extends HTMLElement {
private _highlighter;
private _lang;
private _theme;
private _code;
private _class?;
private _options?;
get highlighter(): HighlighterCore;
set highlighter(value: HighlighterCore);
get lang(): string;
set lang(value: string);
get theme(): string;
set theme(value: string);
get code(): string;
set code(value: string);
get class(): string | undefined;
set class(value: string | undefined);
get options(): (MagicMoveRenderOptions & MagicMoveDifferOptions) | undefined;
set options(value: MagicMoveRenderOptions & MagicMoveDifferOptions | undefined);
private machine?;
private renderer?;
private result?;
private batchUpdate;
private hasUpdated;
constructor();
connectedCallback(): void;
propertyChangedCallback(): void;
updateRenderer(): void;
}
declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move': ShikiMagicMove;
}
}
declare class ShikiMagicMovePrecompiled extends HTMLElement {
private _steps;
private _step;
private _animated;
private _options?;
get steps(): KeyedTokensInfo[];
set steps(value: KeyedTokensInfo[]);
get step(): number;
set step(value: number);
get animated(): boolean;
set animated(value: boolean);
get options(): MagicMoveRenderOptions & MagicMoveDifferOptions | undefined;
set options(value: MagicMoveRenderOptions & MagicMoveDifferOptions);
private renderer?;
private previous;
private batchUpdate;
private hasUpdated;
constructor();
connectedCallback(): void;
propertyChangedCallback(): void;
updateRenderer(): void;
}
declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move-precompiled': ShikiMagicMovePrecompiled;
}
}
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+99
View File
@@ -0,0 +1,99 @@
import { HighlighterCore } from 'shiki/core';
import { KeyedTokensInfo, MagicMoveRenderOptions, MagicMoveDifferOptions } from './types.js';
import 'diff-match-patch-es';
declare class ShikiMagicMoveRenderer extends HTMLElement {
private _animated;
private _tokens;
private _previous?;
private _options?;
private _class;
get animated(): boolean;
set animated(value: boolean);
get tokens(): KeyedTokensInfo;
set tokens(value: KeyedTokensInfo);
get previous(): KeyedTokensInfo | undefined;
set previous(value: KeyedTokensInfo);
get options(): MagicMoveRenderOptions | undefined;
set options(value: MagicMoveRenderOptions | undefined);
get class(): string;
set class(value: string);
private container?;
private renderer?;
private batchUpdate;
private hasUpdated;
constructor();
connectedCallback(): void;
propertyChangedCallback(): void;
transform(): Promise<void>;
}
declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move-renderer': ShikiMagicMoveRenderer;
}
}
declare class ShikiMagicMove extends HTMLElement {
private _highlighter;
private _lang;
private _theme;
private _code;
private _class?;
private _options?;
get highlighter(): HighlighterCore;
set highlighter(value: HighlighterCore);
get lang(): string;
set lang(value: string);
get theme(): string;
set theme(value: string);
get code(): string;
set code(value: string);
get class(): string | undefined;
set class(value: string | undefined);
get options(): (MagicMoveRenderOptions & MagicMoveDifferOptions) | undefined;
set options(value: MagicMoveRenderOptions & MagicMoveDifferOptions | undefined);
private machine?;
private renderer?;
private result?;
private batchUpdate;
private hasUpdated;
constructor();
connectedCallback(): void;
propertyChangedCallback(): void;
updateRenderer(): void;
}
declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move': ShikiMagicMove;
}
}
declare class ShikiMagicMovePrecompiled extends HTMLElement {
private _steps;
private _step;
private _animated;
private _options?;
get steps(): KeyedTokensInfo[];
set steps(value: KeyedTokensInfo[]);
get step(): number;
set step(value: number);
get animated(): boolean;
set animated(value: boolean);
get options(): MagicMoveRenderOptions & MagicMoveDifferOptions | undefined;
set options(value: MagicMoveRenderOptions & MagicMoveDifferOptions);
private renderer?;
private previous;
private batchUpdate;
private hasUpdated;
constructor();
connectedCallback(): void;
propertyChangedCallback(): void;
updateRenderer(): void;
}
declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move-precompiled': ShikiMagicMovePrecompiled;
}
}
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+240
View File
@@ -0,0 +1,240 @@
import { createMagicMoveMachine, codeToKeyedTokens, toKeyedTokens, syncTokenKeys } from './core.mjs';
import { MagicMoveRenderer } from './renderer.mjs';
import 'diff-match-patch-es';
import 'ohash';
class ShikiMagicMoveRenderer extends HTMLElement {
_animated = true;
_class = '';
get animated() {
return this._animated;
}
set animated(value) {
this._animated = value;
this.propertyChangedCallback();
}
get tokens() {
return this._tokens;
}
set tokens(value) {
this._tokens = value;
this.propertyChangedCallback();
}
get previous() {
return this._previous;
}
set previous(value) {
this._previous = value;
this.propertyChangedCallback();
}
get options() {
return this._options;
}
set options(value) {
this._options = value;
if (this.renderer) Object.assign(this.renderer.options, this.options);
this.propertyChangedCallback();
}
get class() {
return this._class;
}
set class(value) {
this._class = value;
this.propertyChangedCallback();
}
batchUpdate = false;
hasUpdated = false;
constructor() {
super();
}
connectedCallback() {
this.container = document.createElement('pre');
this.container.classList.add('shiki-magic-move-container');
this.container.className += ` ${this.class}`;
this.renderer = new MagicMoveRenderer(this.container);
this.renderer.render(this.tokens);
this.appendChild(this.container);
}
propertyChangedCallback() {
if (!this.batchUpdate) {
this.batchUpdate = true;
setTimeout(() => {
this.batchUpdate = false;
if (!this.hasUpdated) {
this.hasUpdated = true;
return;
}
if (this.renderer) this.transform();
}, 0);
}
}
async transform() {
if (!this.renderer) return;
if (!this.animated) return;
if (this.previous && this.previous === this.tokens) return;
if (this.previous) {
this.renderer.replace(this.previous);
this.dispatchEvent(new CustomEvent('onStart'));
await this.renderer?.render(this.tokens);
this.dispatchEvent(new CustomEvent('onEnd'));
} else {
this.renderer.replace(this.tokens);
}
}
}
customElements.define('shiki-magic-move-renderer', ShikiMagicMoveRenderer);
class ShikiMagicMove extends HTMLElement {
get highlighter() {
return this._highlighter;
}
set highlighter(value) {
this._highlighter = value;
this.propertyChangedCallback();
}
get lang() {
return this._lang;
}
set lang(value) {
this._lang = value;
this.propertyChangedCallback();
}
get theme() {
return this._theme;
}
set theme(value) {
this._theme = value;
this.propertyChangedCallback();
}
get code() {
return this._code;
}
set code(value) {
this._code = value;
this.propertyChangedCallback();
}
get class() {
return this._class;
}
set class(value) {
this._class = value;
this.propertyChangedCallback();
}
get options() {
return this._options;
}
set options(value) {
this._options = value;
this.propertyChangedCallback();
}
batchUpdate = false;
hasUpdated = false;
constructor() {
super();
}
connectedCallback() {
this.renderer = document.createElement('shiki-magic-move-renderer');
this.renderer.addEventListener('onStart', () => this.dispatchEvent(new CustomEvent('onStart')));
this.renderer.addEventListener('onEnd', () => this.dispatchEvent(new CustomEvent('onEnd')));
this.machine = createMagicMoveMachine(code => codeToKeyedTokens(this.highlighter, code, {
lang: this.lang,
theme: this.theme
}, this.options?.lineNumbers), this.options);
this.updateRenderer();
this.appendChild(this.renderer);
}
propertyChangedCallback() {
if (!this.batchUpdate) {
this.batchUpdate = true;
setTimeout(() => {
this.batchUpdate = false;
if (!this.hasUpdated) {
this.hasUpdated = true;
return;
}
this.updateRenderer();
}, 0);
}
}
updateRenderer() {
if (this.machine && this.renderer) {
this.result = this.machine.commit(this.code, this.options);
this.renderer.tokens = this.result.current;
this.renderer.previous = this.result.previous;
this.renderer.options = this.options;
this.renderer.class = this.class ?? '';
}
}
}
customElements.define('shiki-magic-move', ShikiMagicMove);
class ShikiMagicMovePrecompiled extends HTMLElement {
_steps = [];
_step = 0;
_animated = true;
get steps() {
return this._steps;
}
set steps(value) {
this._steps = value;
this.propertyChangedCallback();
}
get step() {
return this._step;
}
set step(value) {
this._step = value;
this.propertyChangedCallback();
}
get animated() {
return this._animated;
}
set animated(value) {
this._animated = value;
this.propertyChangedCallback();
}
get options() {
return this._options;
}
set options(value) {
this._options = value;
this.propertyChangedCallback();
}
previous = toKeyedTokens('', []);
batchUpdate = false;
hasUpdated = false;
constructor() {
super();
}
connectedCallback() {
this.renderer = document.createElement('shiki-magic-move-renderer');
this.renderer.addEventListener('onStart', () => this.dispatchEvent(new CustomEvent('onStart')));
this.renderer.addEventListener('onEnd', () => this.dispatchEvent(new CustomEvent('onEnd')));
this.updateRenderer();
this.appendChild(this.renderer);
}
propertyChangedCallback() {
if (!this.batchUpdate) {
this.batchUpdate = true;
setTimeout(() => {
this.batchUpdate = false;
if (!this.hasUpdated) {
this.hasUpdated = true;
return;
}
this.updateRenderer();
}, 0);
}
}
updateRenderer() {
const result = syncTokenKeys(this.previous, this.steps[Math.min(this.step, this.steps.length - 1)], this.options);
this.previous = result.to;
this.renderer.tokens = result.to;
this.renderer.previous = result.from;
this.renderer.options = this.options;
this.renderer.animated = this.animated;
}
}
customElements.define('shiki-magic-move-precompiled', ShikiMagicMovePrecompiled);
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Pooya Parsa <pooya@pi0.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,173 @@
# ohash
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![Github Actions][github-actions-src]][github-actions-href]
[![Codecov][codecov-src]][codecov-href]
[![bundle size][bundle-src]][bundle-href]
> Super fast hashing library written in Vanilla JS
## Usage
Install package:
```sh
# npm
npm install ohash
# yarn
yarn add ohash
# pnpm
pnpm install ohash
```
Import:
```js
// ESM
import { hash, objectHash, murmurHash, sha256 } from "ohash";
// CommonJS
const { hash, objectHash, murmurHash, sha256 } = require("ohash");
```
### `hash(object, options?)`
Converts object value into a string hash using `objectHash` and then applies `sha256` with Base64 encoding (trimmed by length of 10).
Usage:
```js
import { hash } from "ohash";
// "dZbtA7f0lK"
console.log(hash({ foo: "bar" }));
```
### `objectHash(object, options?)`
Converts a nest object value into a stable and safe string for hashing.
Usage:
```js
import { objectHash } from "ohash";
// "object:1:string:3:foo:string:3:bar,"
console.log(objectHash({ foo: "bar" }));
```
### `isEqual(obj1, obj2, options?)`
Compare two objects using reference equality and stable object hashing.
Usage:
```js
import { isEqual } from "ohash";
// true
console.log(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 }));
```
### `diff(obj1, obj2, options?)`
Compare two objects with nested hashing. Returns an array of changes.
Returned value is an array of diff entries with `$key`, `$hash`, `$value` and `$props`. When logging, a string version of changelog is displayed.
Usage:
```js
import { diff } from "ohash";
const createObject = () => ({
foo: "bar",
nested: {
y: 123,
bar: {
baz: "123",
},
},
});
const obj1 = createObject();
const obj2 = createObject();
obj2.nested.x = 123;
delete obj2.nested.y;
obj2.nested.bar.baz = 123;
const diff = diff(obj1, obj2);
// [-] Removed nested.y
// [~] Changed nested.bar.baz from "123" to 123
// [+] Added nested.x
console.log(diff(obj1, obj2));
```
### `murmurHash(str)`
Converts input string (of any length) into a 32-bit positive integer using [MurmurHash3](https://en.wikipedia.org/wiki/MurmurHash).
Usage:
```js
import { murmurHash } from "ohash";
// "427197390"
console.log(murmurHash("Hello World"));
```
### `sha256`
Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest from input string.
```js
import { sha256 } from "ohash";
// "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"
console.log(sha256("Hello World"));
```
### `sha256base64`
Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest in Base64 encoding from input string.
```js
import { sha256base64 } from "ohash";
// "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"
console.log(sha256base64("Hello World"));
```
## 💻 Development
- Clone this repository
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` (use `npm i -g corepack` for Node.js < 16.10)
- Install dependencies using `pnpm install`
- Run interactive tests using `pnpm dev`
## License
Made with 💛
Published under [MIT License](./LICENSE).
Based on [puleos/object-hash](https://github.com/puleos/object-hash) by [Scott Puleo](https://github.com/puleos/), and implementations from [perezd/node-murmurhash](https://github.com/perezd/node-murmurhash) and
[garycourt/murmurhash-js](https://github.com/garycourt/murmurhash-js) by [Gary Court](mailto:gary.court@gmail.com) and [Austin Appleby](mailto:aappleby@gmail.com) and [brix/crypto-js](https://github.com/brix/crypto-js).
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/ohash?style=flat-square
[npm-version-href]: https://npmjs.com/package/ohash
[npm-downloads-src]: https://img.shields.io/npm/dm/ohash?style=flat-square
[npm-downloads-href]: https://npmjs.com/package/ohash
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/unjs/ohash/ci.yml?branch=main&style=flat-square
[github-actions-href]: https://github.com/unjs/ohash/actions?query=workflow%3Aci
[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/ohash/main?style=flat-square
[codecov-href]: https://codecov.io/gh/unjs/ohash
[bundle-src]: https://flat.badgen.net/bundlephobia/minzip/ohash
[bundle-href]: https://bundlephobia.com/package/ohash
@@ -0,0 +1,351 @@
'use strict';
const utils = require('./shared/ohash.BElz9Fxo.cjs');
class WordArray {
words;
sigBytes;
constructor(words, sigBytes) {
words = this.words = words || [];
this.sigBytes = sigBytes === void 0 ? words.length * 4 : sigBytes;
}
toString(encoder) {
return (encoder || Hex).stringify(this);
}
concat(wordArray) {
this.clamp();
if (this.sigBytes % 4) {
for (let i = 0; i < wordArray.sigBytes; i++) {
const thatByte = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255;
this.words[this.sigBytes + i >>> 2] |= thatByte << 24 - (this.sigBytes + i) % 4 * 8;
}
} else {
for (let j = 0; j < wordArray.sigBytes; j += 4) {
this.words[this.sigBytes + j >>> 2] = wordArray.words[j >>> 2];
}
}
this.sigBytes += wordArray.sigBytes;
return this;
}
clamp() {
this.words[this.sigBytes >>> 2] &= 4294967295 << 32 - this.sigBytes % 4 * 8;
this.words.length = Math.ceil(this.sigBytes / 4);
}
clone() {
return new WordArray([...this.words]);
}
}
const Hex = {
stringify(wordArray) {
const hexChars = [];
for (let i = 0; i < wordArray.sigBytes; i++) {
const bite = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255;
hexChars.push((bite >>> 4).toString(16), (bite & 15).toString(16));
}
return hexChars.join("");
}
};
const Base64 = {
stringify(wordArray) {
const keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const base64Chars = [];
for (let i = 0; i < wordArray.sigBytes; i += 3) {
const byte1 = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255;
const byte2 = wordArray.words[i + 1 >>> 2] >>> 24 - (i + 1) % 4 * 8 & 255;
const byte3 = wordArray.words[i + 2 >>> 2] >>> 24 - (i + 2) % 4 * 8 & 255;
const triplet = byte1 << 16 | byte2 << 8 | byte3;
for (let j = 0; j < 4 && i * 8 + j * 6 < wordArray.sigBytes * 8; j++) {
base64Chars.push(keyStr.charAt(triplet >>> 6 * (3 - j) & 63));
}
}
return base64Chars.join("");
}
};
const Latin1 = {
parse(latin1Str) {
const latin1StrLength = latin1Str.length;
const words = [];
for (let i = 0; i < latin1StrLength; i++) {
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 255) << 24 - i % 4 * 8;
}
return new WordArray(words, latin1StrLength);
}
};
const Utf8 = {
parse(utf8Str) {
return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
}
};
class BufferedBlockAlgorithm {
_data = new WordArray();
_nDataBytes = 0;
_minBufferSize = 0;
blockSize = 512 / 32;
reset() {
this._data = new WordArray();
this._nDataBytes = 0;
}
_append(data) {
if (typeof data === "string") {
data = Utf8.parse(data);
}
this._data.concat(data);
this._nDataBytes += data.sigBytes;
}
_doProcessBlock(_dataWords, _offset) {
}
_process(doFlush) {
let processedWords;
let nBlocksReady = this._data.sigBytes / (this.blockSize * 4);
if (doFlush) {
nBlocksReady = Math.ceil(nBlocksReady);
} else {
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
}
const nWordsReady = nBlocksReady * this.blockSize;
const nBytesReady = Math.min(nWordsReady * 4, this._data.sigBytes);
if (nWordsReady) {
for (let offset = 0; offset < nWordsReady; offset += this.blockSize) {
this._doProcessBlock(this._data.words, offset);
}
processedWords = this._data.words.splice(0, nWordsReady);
this._data.sigBytes -= nBytesReady;
}
return new WordArray(processedWords, nBytesReady);
}
}
class Hasher extends BufferedBlockAlgorithm {
update(messageUpdate) {
this._append(messageUpdate);
this._process();
return this;
}
finalize(messageUpdate) {
if (messageUpdate) {
this._append(messageUpdate);
}
}
}
const H = [
1779033703,
-1150833019,
1013904242,
-1521486534,
1359893119,
-1694144372,
528734635,
1541459225
];
const K = [
1116352408,
1899447441,
-1245643825,
-373957723,
961987163,
1508970993,
-1841331548,
-1424204075,
-670586216,
310598401,
607225278,
1426881987,
1925078388,
-2132889090,
-1680079193,
-1046744716,
-459576895,
-272742522,
264347078,
604807628,
770255983,
1249150122,
1555081692,
1996064986,
-1740746414,
-1473132947,
-1341970488,
-1084653625,
-958395405,
-710438585,
113926993,
338241895,
666307205,
773529912,
1294757372,
1396182291,
1695183700,
1986661051,
-2117940946,
-1838011259,
-1564481375,
-1474664885,
-1035236496,
-949202525,
-778901479,
-694614492,
-200395387,
275423344,
430227734,
506948616,
659060556,
883997877,
958139571,
1322822218,
1537002063,
1747873779,
1955562222,
2024104815,
-2067236844,
-1933114872,
-1866530822,
-1538233109,
-1090935817,
-965641998
];
const W = [];
class SHA256 extends Hasher {
_hash = new WordArray([...H]);
/**
* Resets the internal state of the hash object to initial values.
*/
reset() {
super.reset();
this._hash = new WordArray([...H]);
}
_doProcessBlock(M, offset) {
const H2 = this._hash.words;
let a = H2[0];
let b = H2[1];
let c = H2[2];
let d = H2[3];
let e = H2[4];
let f = H2[5];
let g = H2[6];
let h = H2[7];
for (let i = 0; i < 64; i++) {
if (i < 16) {
W[i] = M[offset + i] | 0;
} else {
const gamma0x = W[i - 15];
const gamma0 = (gamma0x << 25 | gamma0x >>> 7) ^ (gamma0x << 14 | gamma0x >>> 18) ^ gamma0x >>> 3;
const gamma1x = W[i - 2];
const gamma1 = (gamma1x << 15 | gamma1x >>> 17) ^ (gamma1x << 13 | gamma1x >>> 19) ^ gamma1x >>> 10;
W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
}
const ch = e & f ^ ~e & g;
const maj = a & b ^ a & c ^ b & c;
const sigma0 = (a << 30 | a >>> 2) ^ (a << 19 | a >>> 13) ^ (a << 10 | a >>> 22);
const sigma1 = (e << 26 | e >>> 6) ^ (e << 21 | e >>> 11) ^ (e << 7 | e >>> 25);
const t1 = h + sigma1 + ch + K[i] + W[i];
const t2 = sigma0 + maj;
h = g;
g = f;
f = e;
e = d + t1 | 0;
d = c;
c = b;
b = a;
a = t1 + t2 | 0;
}
H2[0] = H2[0] + a | 0;
H2[1] = H2[1] + b | 0;
H2[2] = H2[2] + c | 0;
H2[3] = H2[3] + d | 0;
H2[4] = H2[4] + e | 0;
H2[5] = H2[5] + f | 0;
H2[6] = H2[6] + g | 0;
H2[7] = H2[7] + h | 0;
}
/**
* Finishes the hash calculation and returns the hash as a WordArray.
*
* @param {string} messageUpdate - Additional message content to include in the hash.
* @returns {WordArray} The finalised hash as a WordArray.
*/
finalize(messageUpdate) {
super.finalize(messageUpdate);
const nBitsTotal = this._nDataBytes * 8;
const nBitsLeft = this._data.sigBytes * 8;
this._data.words[nBitsLeft >>> 5] |= 128 << 24 - nBitsLeft % 32;
this._data.words[(nBitsLeft + 64 >>> 9 << 4) + 14] = Math.floor(
nBitsTotal / 4294967296
);
this._data.words[(nBitsLeft + 64 >>> 9 << 4) + 15] = nBitsTotal;
this._data.sigBytes = this._data.words.length * 4;
this._process();
return this._hash;
}
}
function sha256(message) {
return new SHA256().finalize(message).toString();
}
function sha256base64(message) {
return new SHA256().finalize(message).toString(Base64);
}
function hash(object, options = {}) {
const hashed = typeof object === "string" ? object : utils.objectHash(object, options);
return sha256base64(hashed).slice(0, 10);
}
function murmurHash(key, seed = 0) {
if (typeof key === "string") {
key = createBuffer(key);
}
let i = 0;
let h1 = seed;
let k1;
let h1b;
const remainder = key.length & 3;
const bytes = key.length - remainder;
const c1 = 3432918353;
const c2 = 461845907;
while (i < bytes) {
k1 = key[i] & 255 | (key[++i] & 255) << 8 | (key[++i] & 255) << 16 | (key[++i] & 255) << 24;
++i;
k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295;
k1 = k1 << 15 | k1 >>> 17;
k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295;
h1 ^= k1;
h1 = h1 << 13 | h1 >>> 19;
h1b = (h1 & 65535) * 5 + (((h1 >>> 16) * 5 & 65535) << 16) & 4294967295;
h1 = (h1b & 65535) + 27492 + (((h1b >>> 16) + 58964 & 65535) << 16);
}
k1 = 0;
switch (remainder) {
case 3: {
k1 ^= (key[i + 2] & 255) << 16;
}
case 2: {
k1 ^= (key[i + 1] & 255) << 8;
}
case 1: {
k1 ^= key[i] & 255;
k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295;
k1 = k1 << 15 | k1 >>> 17;
k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295;
h1 ^= k1;
}
}
h1 ^= key.length;
h1 ^= h1 >>> 16;
h1 = (h1 & 65535) * 2246822507 + (((h1 >>> 16) * 2246822507 & 65535) << 16) & 4294967295;
h1 ^= h1 >>> 13;
h1 = (h1 & 65535) * 3266489909 + (((h1 >>> 16) * 3266489909 & 65535) << 16) & 4294967295;
h1 ^= h1 >>> 16;
return h1 >>> 0;
}
function createBuffer(val) {
return new TextEncoder().encode(val);
}
exports.diff = utils.diff;
exports.isEqual = utils.isEqual;
exports.objectHash = utils.objectHash;
exports.serialize = utils.objectHash;
exports.digest = sha256base64;
exports.hash = hash;
exports.murmurHash = murmurHash;
exports.sha256 = sha256;
exports.sha256base64 = sha256base64;
@@ -0,0 +1,37 @@
import { H as HashOptions } from './shared/ohash.DTXTHv91.cjs';
export { d as diff, i as isEqual, o as objectHash, o as serialize } from './shared/ohash.DTXTHv91.cjs';
/**
* Hash any JS value into a string
* @param {object} object value to hash
* @param {HashOptions} options hashing options. See {@link HashOptions}.
* @return {string} hash value
* @api public
*/
declare function hash(object: any, options?: HashOptions): string;
/**
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
*
* @param {Uint8Array | string} key
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*/
declare function murmurHash(key: Uint8Array | string, seed?: number): number;
/**
* Calculates the SHA-256 hash of the message provided.
*
* @param {string} message - The message to hash.
* @returns {string} The message hash as a hexadecimal string.
*/
declare function sha256(message: string): string;
/**
* Calculates the SHA-256 hash of the given message and encodes it in Base64.
*
* @param {string} message - The message to hash.
* @returns {string} The base64 encoded hash of the message.
*/
declare function sha256base64(message: string): string;
export { sha256base64 as digest, hash, murmurHash, sha256, sha256base64 };
@@ -0,0 +1,37 @@
import { H as HashOptions } from './shared/ohash.DTXTHv91.mjs';
export { d as diff, i as isEqual, o as objectHash, o as serialize } from './shared/ohash.DTXTHv91.mjs';
/**
* Hash any JS value into a string
* @param {object} object value to hash
* @param {HashOptions} options hashing options. See {@link HashOptions}.
* @return {string} hash value
* @api public
*/
declare function hash(object: any, options?: HashOptions): string;
/**
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
*
* @param {Uint8Array | string} key
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*/
declare function murmurHash(key: Uint8Array | string, seed?: number): number;
/**
* Calculates the SHA-256 hash of the message provided.
*
* @param {string} message - The message to hash.
* @returns {string} The message hash as a hexadecimal string.
*/
declare function sha256(message: string): string;
/**
* Calculates the SHA-256 hash of the given message and encodes it in Base64.
*
* @param {string} message - The message to hash.
* @returns {string} The base64 encoded hash of the message.
*/
declare function sha256base64(message: string): string;
export { sha256base64 as digest, hash, murmurHash, sha256, sha256base64 };
@@ -0,0 +1,37 @@
import { H as HashOptions } from './shared/ohash.DTXTHv91.js';
export { d as diff, i as isEqual, o as objectHash, o as serialize } from './shared/ohash.DTXTHv91.js';
/**
* Hash any JS value into a string
* @param {object} object value to hash
* @param {HashOptions} options hashing options. See {@link HashOptions}.
* @return {string} hash value
* @api public
*/
declare function hash(object: any, options?: HashOptions): string;
/**
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
*
* @param {Uint8Array | string} key
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*/
declare function murmurHash(key: Uint8Array | string, seed?: number): number;
/**
* Calculates the SHA-256 hash of the message provided.
*
* @param {string} message - The message to hash.
* @returns {string} The message hash as a hexadecimal string.
*/
declare function sha256(message: string): string;
/**
* Calculates the SHA-256 hash of the given message and encodes it in Base64.
*
* @param {string} message - The message to hash.
* @returns {string} The base64 encoded hash of the message.
*/
declare function sha256base64(message: string): string;
export { sha256base64 as digest, hash, murmurHash, sha256, sha256base64 };
@@ -0,0 +1,342 @@
import { o as objectHash } from './shared/ohash.BvSMZzli.mjs';
export { d as diff, i as isEqual } from './shared/ohash.BvSMZzli.mjs';
class WordArray {
words;
sigBytes;
constructor(words, sigBytes) {
words = this.words = words || [];
this.sigBytes = sigBytes === void 0 ? words.length * 4 : sigBytes;
}
toString(encoder) {
return (encoder || Hex).stringify(this);
}
concat(wordArray) {
this.clamp();
if (this.sigBytes % 4) {
for (let i = 0; i < wordArray.sigBytes; i++) {
const thatByte = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255;
this.words[this.sigBytes + i >>> 2] |= thatByte << 24 - (this.sigBytes + i) % 4 * 8;
}
} else {
for (let j = 0; j < wordArray.sigBytes; j += 4) {
this.words[this.sigBytes + j >>> 2] = wordArray.words[j >>> 2];
}
}
this.sigBytes += wordArray.sigBytes;
return this;
}
clamp() {
this.words[this.sigBytes >>> 2] &= 4294967295 << 32 - this.sigBytes % 4 * 8;
this.words.length = Math.ceil(this.sigBytes / 4);
}
clone() {
return new WordArray([...this.words]);
}
}
const Hex = {
stringify(wordArray) {
const hexChars = [];
for (let i = 0; i < wordArray.sigBytes; i++) {
const bite = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255;
hexChars.push((bite >>> 4).toString(16), (bite & 15).toString(16));
}
return hexChars.join("");
}
};
const Base64 = {
stringify(wordArray) {
const keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const base64Chars = [];
for (let i = 0; i < wordArray.sigBytes; i += 3) {
const byte1 = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255;
const byte2 = wordArray.words[i + 1 >>> 2] >>> 24 - (i + 1) % 4 * 8 & 255;
const byte3 = wordArray.words[i + 2 >>> 2] >>> 24 - (i + 2) % 4 * 8 & 255;
const triplet = byte1 << 16 | byte2 << 8 | byte3;
for (let j = 0; j < 4 && i * 8 + j * 6 < wordArray.sigBytes * 8; j++) {
base64Chars.push(keyStr.charAt(triplet >>> 6 * (3 - j) & 63));
}
}
return base64Chars.join("");
}
};
const Latin1 = {
parse(latin1Str) {
const latin1StrLength = latin1Str.length;
const words = [];
for (let i = 0; i < latin1StrLength; i++) {
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 255) << 24 - i % 4 * 8;
}
return new WordArray(words, latin1StrLength);
}
};
const Utf8 = {
parse(utf8Str) {
return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
}
};
class BufferedBlockAlgorithm {
_data = new WordArray();
_nDataBytes = 0;
_minBufferSize = 0;
blockSize = 512 / 32;
reset() {
this._data = new WordArray();
this._nDataBytes = 0;
}
_append(data) {
if (typeof data === "string") {
data = Utf8.parse(data);
}
this._data.concat(data);
this._nDataBytes += data.sigBytes;
}
_doProcessBlock(_dataWords, _offset) {
}
_process(doFlush) {
let processedWords;
let nBlocksReady = this._data.sigBytes / (this.blockSize * 4);
if (doFlush) {
nBlocksReady = Math.ceil(nBlocksReady);
} else {
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
}
const nWordsReady = nBlocksReady * this.blockSize;
const nBytesReady = Math.min(nWordsReady * 4, this._data.sigBytes);
if (nWordsReady) {
for (let offset = 0; offset < nWordsReady; offset += this.blockSize) {
this._doProcessBlock(this._data.words, offset);
}
processedWords = this._data.words.splice(0, nWordsReady);
this._data.sigBytes -= nBytesReady;
}
return new WordArray(processedWords, nBytesReady);
}
}
class Hasher extends BufferedBlockAlgorithm {
update(messageUpdate) {
this._append(messageUpdate);
this._process();
return this;
}
finalize(messageUpdate) {
if (messageUpdate) {
this._append(messageUpdate);
}
}
}
const H = [
1779033703,
-1150833019,
1013904242,
-1521486534,
1359893119,
-1694144372,
528734635,
1541459225
];
const K = [
1116352408,
1899447441,
-1245643825,
-373957723,
961987163,
1508970993,
-1841331548,
-1424204075,
-670586216,
310598401,
607225278,
1426881987,
1925078388,
-2132889090,
-1680079193,
-1046744716,
-459576895,
-272742522,
264347078,
604807628,
770255983,
1249150122,
1555081692,
1996064986,
-1740746414,
-1473132947,
-1341970488,
-1084653625,
-958395405,
-710438585,
113926993,
338241895,
666307205,
773529912,
1294757372,
1396182291,
1695183700,
1986661051,
-2117940946,
-1838011259,
-1564481375,
-1474664885,
-1035236496,
-949202525,
-778901479,
-694614492,
-200395387,
275423344,
430227734,
506948616,
659060556,
883997877,
958139571,
1322822218,
1537002063,
1747873779,
1955562222,
2024104815,
-2067236844,
-1933114872,
-1866530822,
-1538233109,
-1090935817,
-965641998
];
const W = [];
class SHA256 extends Hasher {
_hash = new WordArray([...H]);
/**
* Resets the internal state of the hash object to initial values.
*/
reset() {
super.reset();
this._hash = new WordArray([...H]);
}
_doProcessBlock(M, offset) {
const H2 = this._hash.words;
let a = H2[0];
let b = H2[1];
let c = H2[2];
let d = H2[3];
let e = H2[4];
let f = H2[5];
let g = H2[6];
let h = H2[7];
for (let i = 0; i < 64; i++) {
if (i < 16) {
W[i] = M[offset + i] | 0;
} else {
const gamma0x = W[i - 15];
const gamma0 = (gamma0x << 25 | gamma0x >>> 7) ^ (gamma0x << 14 | gamma0x >>> 18) ^ gamma0x >>> 3;
const gamma1x = W[i - 2];
const gamma1 = (gamma1x << 15 | gamma1x >>> 17) ^ (gamma1x << 13 | gamma1x >>> 19) ^ gamma1x >>> 10;
W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
}
const ch = e & f ^ ~e & g;
const maj = a & b ^ a & c ^ b & c;
const sigma0 = (a << 30 | a >>> 2) ^ (a << 19 | a >>> 13) ^ (a << 10 | a >>> 22);
const sigma1 = (e << 26 | e >>> 6) ^ (e << 21 | e >>> 11) ^ (e << 7 | e >>> 25);
const t1 = h + sigma1 + ch + K[i] + W[i];
const t2 = sigma0 + maj;
h = g;
g = f;
f = e;
e = d + t1 | 0;
d = c;
c = b;
b = a;
a = t1 + t2 | 0;
}
H2[0] = H2[0] + a | 0;
H2[1] = H2[1] + b | 0;
H2[2] = H2[2] + c | 0;
H2[3] = H2[3] + d | 0;
H2[4] = H2[4] + e | 0;
H2[5] = H2[5] + f | 0;
H2[6] = H2[6] + g | 0;
H2[7] = H2[7] + h | 0;
}
/**
* Finishes the hash calculation and returns the hash as a WordArray.
*
* @param {string} messageUpdate - Additional message content to include in the hash.
* @returns {WordArray} The finalised hash as a WordArray.
*/
finalize(messageUpdate) {
super.finalize(messageUpdate);
const nBitsTotal = this._nDataBytes * 8;
const nBitsLeft = this._data.sigBytes * 8;
this._data.words[nBitsLeft >>> 5] |= 128 << 24 - nBitsLeft % 32;
this._data.words[(nBitsLeft + 64 >>> 9 << 4) + 14] = Math.floor(
nBitsTotal / 4294967296
);
this._data.words[(nBitsLeft + 64 >>> 9 << 4) + 15] = nBitsTotal;
this._data.sigBytes = this._data.words.length * 4;
this._process();
return this._hash;
}
}
function sha256(message) {
return new SHA256().finalize(message).toString();
}
function sha256base64(message) {
return new SHA256().finalize(message).toString(Base64);
}
function hash(object, options = {}) {
const hashed = typeof object === "string" ? object : objectHash(object, options);
return sha256base64(hashed).slice(0, 10);
}
function murmurHash(key, seed = 0) {
if (typeof key === "string") {
key = createBuffer(key);
}
let i = 0;
let h1 = seed;
let k1;
let h1b;
const remainder = key.length & 3;
const bytes = key.length - remainder;
const c1 = 3432918353;
const c2 = 461845907;
while (i < bytes) {
k1 = key[i] & 255 | (key[++i] & 255) << 8 | (key[++i] & 255) << 16 | (key[++i] & 255) << 24;
++i;
k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295;
k1 = k1 << 15 | k1 >>> 17;
k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295;
h1 ^= k1;
h1 = h1 << 13 | h1 >>> 19;
h1b = (h1 & 65535) * 5 + (((h1 >>> 16) * 5 & 65535) << 16) & 4294967295;
h1 = (h1b & 65535) + 27492 + (((h1b >>> 16) + 58964 & 65535) << 16);
}
k1 = 0;
switch (remainder) {
case 3: {
k1 ^= (key[i + 2] & 255) << 16;
}
case 2: {
k1 ^= (key[i + 1] & 255) << 8;
}
case 1: {
k1 ^= key[i] & 255;
k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295;
k1 = k1 << 15 | k1 >>> 17;
k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295;
h1 ^= k1;
}
}
h1 ^= key.length;
h1 ^= h1 >>> 16;
h1 = (h1 & 65535) * 2246822507 + (((h1 >>> 16) * 2246822507 & 65535) << 16) & 4294967295;
h1 ^= h1 >>> 13;
h1 = (h1 & 65535) * 3266489909 + (((h1 >>> 16) * 3266489909 & 65535) << 16) & 4294967295;
h1 ^= h1 >>> 16;
return h1 >>> 0;
}
function createBuffer(val) {
return new TextEncoder().encode(val);
}
export { sha256base64 as digest, hash, murmurHash, objectHash, objectHash as serialize, sha256, sha256base64 };
@@ -0,0 +1,432 @@
'use strict';
const defaults = Object.freeze({
ignoreUnknown: false,
respectType: false,
respectFunctionNames: false,
respectFunctionProperties: false,
unorderedObjects: true,
unorderedArrays: false,
unorderedSets: false,
excludeKeys: void 0,
excludeValues: void 0,
replacer: void 0
});
function objectHash(object, options) {
if (options) {
options = { ...defaults, ...options };
} else {
options = defaults;
}
const hasher = createHasher(options);
hasher.dispatch(object);
return hasher.toString();
}
const defaultPrototypesKeys = Object.freeze([
"prototype",
"__proto__",
"constructor"
]);
function createHasher(options) {
let buff = "";
let context = /* @__PURE__ */ new Map();
const write = (str) => {
buff += str;
};
return {
toString() {
return buff;
},
getContext() {
return context;
},
dispatch(value) {
if (options.replacer) {
value = options.replacer(value);
}
const type = value === null ? "null" : typeof value;
return this[type](value);
},
object(object) {
if (object && typeof object.toJSON === "function") {
return this.object(object.toJSON());
}
const objString = Object.prototype.toString.call(object);
let objType = "";
const objectLength = objString.length;
if (objectLength < 10) {
objType = "unknown:[" + objString + "]";
} else {
objType = objString.slice(8, objectLength - 1);
}
objType = objType.toLowerCase();
let objectNumber = null;
if ((objectNumber = context.get(object)) === void 0) {
context.set(object, context.size);
} else {
return this.dispatch("[CIRCULAR:" + objectNumber + "]");
}
if (typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(object)) {
write("buffer:");
return write(object.toString("utf8"));
}
if (objType !== "object" && objType !== "function" && objType !== "asyncfunction") {
if (this[objType]) {
this[objType](object);
} else if (!options.ignoreUnknown) {
this.unkown(object, objType);
}
} else {
let keys = Object.keys(object);
if (options.unorderedObjects) {
keys = keys.sort();
}
let extraKeys = [];
if (options.respectType !== false && !isNativeFunction(object)) {
extraKeys = defaultPrototypesKeys;
}
if (options.excludeKeys) {
keys = keys.filter((key) => {
return !options.excludeKeys(key);
});
extraKeys = extraKeys.filter((key) => {
return !options.excludeKeys(key);
});
}
write("object:" + (keys.length + extraKeys.length) + ":");
const dispatchForKey = (key) => {
this.dispatch(key);
write(":");
if (!options.excludeValues) {
this.dispatch(object[key]);
}
write(",");
};
for (const key of keys) {
dispatchForKey(key);
}
for (const key of extraKeys) {
dispatchForKey(key);
}
}
},
array(arr, unordered) {
unordered = unordered === void 0 ? options.unorderedArrays !== false : unordered;
write("array:" + arr.length + ":");
if (!unordered || arr.length <= 1) {
for (const entry of arr) {
this.dispatch(entry);
}
return;
}
const contextAdditions = /* @__PURE__ */ new Map();
const entries = arr.map((entry) => {
const hasher = createHasher(options);
hasher.dispatch(entry);
for (const [key, value] of hasher.getContext()) {
contextAdditions.set(key, value);
}
return hasher.toString();
});
context = contextAdditions;
entries.sort();
return this.array(entries, false);
},
date(date) {
return write("date:" + date.toJSON());
},
symbol(sym) {
return write("symbol:" + sym.toString());
},
unkown(value, type) {
write(type);
if (!value) {
return;
}
write(":");
if (value && typeof value.entries === "function") {
return this.array(
Array.from(value.entries()),
true
/* ordered */
);
}
},
error(err) {
return write("error:" + err.toString());
},
boolean(bool) {
return write("bool:" + bool);
},
string(string) {
write("string:" + string.length + ":");
write(string);
},
function(fn) {
write("fn:");
if (isNativeFunction(fn)) {
this.dispatch("[native]");
} else {
this.dispatch(fn.toString());
}
if (options.respectFunctionNames !== false) {
this.dispatch("function-name:" + String(fn.name));
}
if (options.respectFunctionProperties) {
this.object(fn);
}
},
number(number) {
return write("number:" + number);
},
xml(xml) {
return write("xml:" + xml.toString());
},
null() {
return write("Null");
},
undefined() {
return write("Undefined");
},
regexp(regex) {
return write("regex:" + regex.toString());
},
uint8array(arr) {
write("uint8array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
uint8clampedarray(arr) {
write("uint8clampedarray:");
return this.dispatch(Array.prototype.slice.call(arr));
},
int8array(arr) {
write("int8array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
uint16array(arr) {
write("uint16array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
int16array(arr) {
write("int16array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
uint32array(arr) {
write("uint32array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
int32array(arr) {
write("int32array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
float32array(arr) {
write("float32array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
float64array(arr) {
write("float64array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
arraybuffer(arr) {
write("arraybuffer:");
return this.dispatch(new Uint8Array(arr));
},
url(url) {
return write("url:" + url.toString());
},
map(map) {
write("map:");
const arr = [...map];
return this.array(arr, options.unorderedSets !== false);
},
set(set) {
write("set:");
const arr = [...set];
return this.array(arr, options.unorderedSets !== false);
},
file(file) {
write("file:");
return this.dispatch([file.name, file.size, file.type, file.lastModfied]);
},
blob() {
if (options.ignoreUnknown) {
return write("[blob]");
}
throw new Error(
'Hashing Blob objects is currently not supported\nUse "options.replacer" or "options.ignoreUnknown"\n'
);
},
domwindow() {
return write("domwindow");
},
bigint(number) {
return write("bigint:" + number.toString());
},
/* Node.js standard native objects */
process() {
return write("process");
},
timer() {
return write("timer");
},
pipe() {
return write("pipe");
},
tcp() {
return write("tcp");
},
udp() {
return write("udp");
},
tty() {
return write("tty");
},
statwatcher() {
return write("statwatcher");
},
securecontext() {
return write("securecontext");
},
connection() {
return write("connection");
},
zlib() {
return write("zlib");
},
context() {
return write("context");
},
nodescript() {
return write("nodescript");
},
httpparser() {
return write("httpparser");
},
dataview() {
return write("dataview");
},
signal() {
return write("signal");
},
fsevent() {
return write("fsevent");
},
tlswrap() {
return write("tlswrap");
}
};
}
const nativeFunc = "[native code] }";
const nativeFuncLength = nativeFunc.length;
function isNativeFunction(f) {
if (typeof f !== "function") {
return false;
}
return Function.prototype.toString.call(f).slice(-nativeFuncLength) === nativeFunc;
}
function diff(obj1, obj2, opts = {}) {
const h1 = _toHashedObject(obj1, opts);
const h2 = _toHashedObject(obj2, opts);
return _diff(h1, h2, opts);
}
function _diff(h1, h2, opts = {}) {
const diffs = [];
const allProps = /* @__PURE__ */ new Set([
...Object.keys(h1.props || {}),
...Object.keys(h2.props || {})
]);
if (h1.props && h2.props) {
for (const prop of allProps) {
const p1 = h1.props[prop];
const p2 = h2.props[prop];
if (p1 && p2) {
diffs.push(..._diff(h1.props?.[prop], h2.props?.[prop], opts));
} else if (p1 || p2) {
diffs.push(
new DiffEntry((p2 || p1).key, p1 ? "removed" : "added", p2, p1)
);
}
}
}
if (allProps.size === 0 && h1.hash !== h2.hash) {
diffs.push(new DiffEntry((h2 || h1).key, "changed", h2, h1));
}
return diffs;
}
function _toHashedObject(obj, opts, key = "") {
if (obj && typeof obj !== "object") {
return new DiffHashedObject(key, obj, objectHash(obj, opts));
}
const props = {};
const hashes = [];
for (const _key in obj) {
props[_key] = _toHashedObject(
obj[_key],
opts,
key ? `${key}.${_key}` : _key
);
hashes.push(props[_key].hash);
}
return new DiffHashedObject(key, obj, `{${hashes.join(":")}}`, props);
}
class DiffEntry {
constructor(key, type, newValue, oldValue) {
this.key = key;
this.type = type;
this.newValue = newValue;
this.oldValue = oldValue;
}
toString() {
return this.toJSON();
}
toJSON() {
switch (this.type) {
case "added": {
return `Added \`${this.key}\``;
}
case "removed": {
return `Removed \`${this.key}\``;
}
case "changed": {
return `Changed \`${this.key}\` from \`${this.oldValue?.toString() || "-"}\` to \`${this.newValue.toString()}\``;
}
}
}
}
class DiffHashedObject {
constructor(key, value, hash, props) {
this.key = key;
this.value = value;
this.hash = hash;
this.props = props;
}
toString() {
if (this.props) {
return `{${Object.keys(this.props).join(",")}}`;
} else {
return JSON.stringify(this.value);
}
}
toJSON() {
const k = this.key || ".";
if (this.props) {
return `${k}({${Object.keys(this.props).join(",")}})`;
}
return `${k}(${this.value})`;
}
}
function isEqual(object1, object2, hashOptions = {}) {
if (object1 === object2) {
return true;
}
if (objectHash(object1, hashOptions) === objectHash(object2, hashOptions)) {
return true;
}
return false;
}
exports.diff = diff;
exports.isEqual = isEqual;
exports.objectHash = objectHash;
@@ -0,0 +1,428 @@
const defaults = Object.freeze({
ignoreUnknown: false,
respectType: false,
respectFunctionNames: false,
respectFunctionProperties: false,
unorderedObjects: true,
unorderedArrays: false,
unorderedSets: false,
excludeKeys: void 0,
excludeValues: void 0,
replacer: void 0
});
function objectHash(object, options) {
if (options) {
options = { ...defaults, ...options };
} else {
options = defaults;
}
const hasher = createHasher(options);
hasher.dispatch(object);
return hasher.toString();
}
const defaultPrototypesKeys = Object.freeze([
"prototype",
"__proto__",
"constructor"
]);
function createHasher(options) {
let buff = "";
let context = /* @__PURE__ */ new Map();
const write = (str) => {
buff += str;
};
return {
toString() {
return buff;
},
getContext() {
return context;
},
dispatch(value) {
if (options.replacer) {
value = options.replacer(value);
}
const type = value === null ? "null" : typeof value;
return this[type](value);
},
object(object) {
if (object && typeof object.toJSON === "function") {
return this.object(object.toJSON());
}
const objString = Object.prototype.toString.call(object);
let objType = "";
const objectLength = objString.length;
if (objectLength < 10) {
objType = "unknown:[" + objString + "]";
} else {
objType = objString.slice(8, objectLength - 1);
}
objType = objType.toLowerCase();
let objectNumber = null;
if ((objectNumber = context.get(object)) === void 0) {
context.set(object, context.size);
} else {
return this.dispatch("[CIRCULAR:" + objectNumber + "]");
}
if (typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(object)) {
write("buffer:");
return write(object.toString("utf8"));
}
if (objType !== "object" && objType !== "function" && objType !== "asyncfunction") {
if (this[objType]) {
this[objType](object);
} else if (!options.ignoreUnknown) {
this.unkown(object, objType);
}
} else {
let keys = Object.keys(object);
if (options.unorderedObjects) {
keys = keys.sort();
}
let extraKeys = [];
if (options.respectType !== false && !isNativeFunction(object)) {
extraKeys = defaultPrototypesKeys;
}
if (options.excludeKeys) {
keys = keys.filter((key) => {
return !options.excludeKeys(key);
});
extraKeys = extraKeys.filter((key) => {
return !options.excludeKeys(key);
});
}
write("object:" + (keys.length + extraKeys.length) + ":");
const dispatchForKey = (key) => {
this.dispatch(key);
write(":");
if (!options.excludeValues) {
this.dispatch(object[key]);
}
write(",");
};
for (const key of keys) {
dispatchForKey(key);
}
for (const key of extraKeys) {
dispatchForKey(key);
}
}
},
array(arr, unordered) {
unordered = unordered === void 0 ? options.unorderedArrays !== false : unordered;
write("array:" + arr.length + ":");
if (!unordered || arr.length <= 1) {
for (const entry of arr) {
this.dispatch(entry);
}
return;
}
const contextAdditions = /* @__PURE__ */ new Map();
const entries = arr.map((entry) => {
const hasher = createHasher(options);
hasher.dispatch(entry);
for (const [key, value] of hasher.getContext()) {
contextAdditions.set(key, value);
}
return hasher.toString();
});
context = contextAdditions;
entries.sort();
return this.array(entries, false);
},
date(date) {
return write("date:" + date.toJSON());
},
symbol(sym) {
return write("symbol:" + sym.toString());
},
unkown(value, type) {
write(type);
if (!value) {
return;
}
write(":");
if (value && typeof value.entries === "function") {
return this.array(
Array.from(value.entries()),
true
/* ordered */
);
}
},
error(err) {
return write("error:" + err.toString());
},
boolean(bool) {
return write("bool:" + bool);
},
string(string) {
write("string:" + string.length + ":");
write(string);
},
function(fn) {
write("fn:");
if (isNativeFunction(fn)) {
this.dispatch("[native]");
} else {
this.dispatch(fn.toString());
}
if (options.respectFunctionNames !== false) {
this.dispatch("function-name:" + String(fn.name));
}
if (options.respectFunctionProperties) {
this.object(fn);
}
},
number(number) {
return write("number:" + number);
},
xml(xml) {
return write("xml:" + xml.toString());
},
null() {
return write("Null");
},
undefined() {
return write("Undefined");
},
regexp(regex) {
return write("regex:" + regex.toString());
},
uint8array(arr) {
write("uint8array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
uint8clampedarray(arr) {
write("uint8clampedarray:");
return this.dispatch(Array.prototype.slice.call(arr));
},
int8array(arr) {
write("int8array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
uint16array(arr) {
write("uint16array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
int16array(arr) {
write("int16array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
uint32array(arr) {
write("uint32array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
int32array(arr) {
write("int32array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
float32array(arr) {
write("float32array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
float64array(arr) {
write("float64array:");
return this.dispatch(Array.prototype.slice.call(arr));
},
arraybuffer(arr) {
write("arraybuffer:");
return this.dispatch(new Uint8Array(arr));
},
url(url) {
return write("url:" + url.toString());
},
map(map) {
write("map:");
const arr = [...map];
return this.array(arr, options.unorderedSets !== false);
},
set(set) {
write("set:");
const arr = [...set];
return this.array(arr, options.unorderedSets !== false);
},
file(file) {
write("file:");
return this.dispatch([file.name, file.size, file.type, file.lastModfied]);
},
blob() {
if (options.ignoreUnknown) {
return write("[blob]");
}
throw new Error(
'Hashing Blob objects is currently not supported\nUse "options.replacer" or "options.ignoreUnknown"\n'
);
},
domwindow() {
return write("domwindow");
},
bigint(number) {
return write("bigint:" + number.toString());
},
/* Node.js standard native objects */
process() {
return write("process");
},
timer() {
return write("timer");
},
pipe() {
return write("pipe");
},
tcp() {
return write("tcp");
},
udp() {
return write("udp");
},
tty() {
return write("tty");
},
statwatcher() {
return write("statwatcher");
},
securecontext() {
return write("securecontext");
},
connection() {
return write("connection");
},
zlib() {
return write("zlib");
},
context() {
return write("context");
},
nodescript() {
return write("nodescript");
},
httpparser() {
return write("httpparser");
},
dataview() {
return write("dataview");
},
signal() {
return write("signal");
},
fsevent() {
return write("fsevent");
},
tlswrap() {
return write("tlswrap");
}
};
}
const nativeFunc = "[native code] }";
const nativeFuncLength = nativeFunc.length;
function isNativeFunction(f) {
if (typeof f !== "function") {
return false;
}
return Function.prototype.toString.call(f).slice(-nativeFuncLength) === nativeFunc;
}
function diff(obj1, obj2, opts = {}) {
const h1 = _toHashedObject(obj1, opts);
const h2 = _toHashedObject(obj2, opts);
return _diff(h1, h2, opts);
}
function _diff(h1, h2, opts = {}) {
const diffs = [];
const allProps = /* @__PURE__ */ new Set([
...Object.keys(h1.props || {}),
...Object.keys(h2.props || {})
]);
if (h1.props && h2.props) {
for (const prop of allProps) {
const p1 = h1.props[prop];
const p2 = h2.props[prop];
if (p1 && p2) {
diffs.push(..._diff(h1.props?.[prop], h2.props?.[prop], opts));
} else if (p1 || p2) {
diffs.push(
new DiffEntry((p2 || p1).key, p1 ? "removed" : "added", p2, p1)
);
}
}
}
if (allProps.size === 0 && h1.hash !== h2.hash) {
diffs.push(new DiffEntry((h2 || h1).key, "changed", h2, h1));
}
return diffs;
}
function _toHashedObject(obj, opts, key = "") {
if (obj && typeof obj !== "object") {
return new DiffHashedObject(key, obj, objectHash(obj, opts));
}
const props = {};
const hashes = [];
for (const _key in obj) {
props[_key] = _toHashedObject(
obj[_key],
opts,
key ? `${key}.${_key}` : _key
);
hashes.push(props[_key].hash);
}
return new DiffHashedObject(key, obj, `{${hashes.join(":")}}`, props);
}
class DiffEntry {
constructor(key, type, newValue, oldValue) {
this.key = key;
this.type = type;
this.newValue = newValue;
this.oldValue = oldValue;
}
toString() {
return this.toJSON();
}
toJSON() {
switch (this.type) {
case "added": {
return `Added \`${this.key}\``;
}
case "removed": {
return `Removed \`${this.key}\``;
}
case "changed": {
return `Changed \`${this.key}\` from \`${this.oldValue?.toString() || "-"}\` to \`${this.newValue.toString()}\``;
}
}
}
}
class DiffHashedObject {
constructor(key, value, hash, props) {
this.key = key;
this.value = value;
this.hash = hash;
this.props = props;
}
toString() {
if (this.props) {
return `{${Object.keys(this.props).join(",")}}`;
} else {
return JSON.stringify(this.value);
}
}
toJSON() {
const k = this.key || ".";
if (this.props) {
return `${k}({${Object.keys(this.props).join(",")}})`;
}
return `${k}(${this.value})`;
}
}
function isEqual(object1, object2, hashOptions = {}) {
if (object1 === object2) {
return true;
}
if (objectHash(object1, hashOptions) === objectHash(object2, hashOptions)) {
return true;
}
return false;
}
export { diff as d, isEqual as i, objectHash as o };
@@ -0,0 +1,111 @@
interface HashOptions {
/**
* Function to determine if a key should be excluded from hashing.
* @optional
* @param key - The key to check for exclusion.
* @returns {boolean} - Returns true to exclude the key from hashing.
*/
excludeKeys?: ((key: string) => boolean) | undefined;
/**
* Specifies whether to exclude values from hashing, so that only the object keys are hashed.
* @optional
*/
excludeValues?: boolean | undefined;
/**
* Specifies whether to ignore objects of unknown type (not directly serialisable) when hashing.
* @optional
* @default false
*/
ignoreUnknown?: boolean | undefined;
/**
* A function that replaces values before they are hashed, which can be used to customise the hashing process.
* @optional
* @param value - The current value to be hashed.
* @returns {any} - The value to use for hashing instead.
*/
replacer?: ((value: any) => any) | undefined;
/**
* Specifies whether the 'name' property of functions should be taken into account when hashing.
* @optional
* @default false
*/
respectFunctionNames?: boolean | undefined;
/**
* Specifies whether properties of functions should be taken into account when hashing.
* @optional
* @default false
*/
respectFunctionProperties?: boolean | undefined;
/**
* Specifies whether to include type-specific properties such as prototype or constructor in the hash to distinguish between types.
* @optional
* @default false
*/
respectType?: boolean | undefined;
/**
* Specifies whether arrays should be sorted before hashing to ensure consistent order.
* @optional
* @default false
*/
unorderedArrays?: boolean | undefined;
/**
* Specifies whether Set and Map instances should be sorted by key before hashing to ensure consistent order.
* @optional
* @default true
*/
unorderedObjects?: boolean | undefined;
/**
* Specifies whether the elements of `Set' and keys of `Map' should be sorted before hashing to ensure consistent order.
* @optional
* @default false
*/
unorderedSets?: boolean | undefined;
}
/**
* Serialize any JS value into a stable, hashable string
* @param {object} object value to hash
* @param {HashOptions} options hashing options. See {@link HashOptions}.
* @return {string} serialized value
* @api public
*/
declare function objectHash(object: any, options?: HashOptions): string;
/**
* Calculates the difference between two objects and returns a list of differences.
*
* @param {any} obj1 - The first object to compare.
* @param {any} obj2 - The second object to compare.
* @param {HashOptions} [opts={}] - Configuration options for hashing the objects. See {@link HashOptions}.
* @returns {DiffEntry[]} An array with the differences between the two objects.
*/
declare function diff(obj1: any, obj2: any, opts?: HashOptions): DiffEntry[];
declare class DiffEntry {
key: string;
type: "changed" | "added" | "removed";
newValue: DiffHashedObject;
oldValue?: DiffHashedObject | undefined;
constructor(key: string, type: "changed" | "added" | "removed", newValue: DiffHashedObject, oldValue?: DiffHashedObject | undefined);
toString(): string;
toJSON(): string;
}
declare class DiffHashedObject {
key: string;
value: any;
hash?: string | undefined;
props?: Record<string, DiffHashedObject> | undefined;
constructor(key: string, value: any, hash?: string | undefined, props?: Record<string, DiffHashedObject> | undefined);
toString(): string;
toJSON(): string;
}
/**
* Compare two objects using reference equality and stable deep hashing.
* @param {any} object1 First object
* @param {any} object2 Second object
* @param {HashOptions} hashOptions. Configuration options for hashing the objects. See {@link HashOptions}.
* @return {boolean} true if equal and false if not
* @api public
*/
declare function isEqual(object1: any, object2: any, hashOptions?: HashOptions): boolean;
export { type HashOptions as H, diff as d, isEqual as i, objectHash as o };
@@ -0,0 +1,111 @@
interface HashOptions {
/**
* Function to determine if a key should be excluded from hashing.
* @optional
* @param key - The key to check for exclusion.
* @returns {boolean} - Returns true to exclude the key from hashing.
*/
excludeKeys?: ((key: string) => boolean) | undefined;
/**
* Specifies whether to exclude values from hashing, so that only the object keys are hashed.
* @optional
*/
excludeValues?: boolean | undefined;
/**
* Specifies whether to ignore objects of unknown type (not directly serialisable) when hashing.
* @optional
* @default false
*/
ignoreUnknown?: boolean | undefined;
/**
* A function that replaces values before they are hashed, which can be used to customise the hashing process.
* @optional
* @param value - The current value to be hashed.
* @returns {any} - The value to use for hashing instead.
*/
replacer?: ((value: any) => any) | undefined;
/**
* Specifies whether the 'name' property of functions should be taken into account when hashing.
* @optional
* @default false
*/
respectFunctionNames?: boolean | undefined;
/**
* Specifies whether properties of functions should be taken into account when hashing.
* @optional
* @default false
*/
respectFunctionProperties?: boolean | undefined;
/**
* Specifies whether to include type-specific properties such as prototype or constructor in the hash to distinguish between types.
* @optional
* @default false
*/
respectType?: boolean | undefined;
/**
* Specifies whether arrays should be sorted before hashing to ensure consistent order.
* @optional
* @default false
*/
unorderedArrays?: boolean | undefined;
/**
* Specifies whether Set and Map instances should be sorted by key before hashing to ensure consistent order.
* @optional
* @default true
*/
unorderedObjects?: boolean | undefined;
/**
* Specifies whether the elements of `Set' and keys of `Map' should be sorted before hashing to ensure consistent order.
* @optional
* @default false
*/
unorderedSets?: boolean | undefined;
}
/**
* Serialize any JS value into a stable, hashable string
* @param {object} object value to hash
* @param {HashOptions} options hashing options. See {@link HashOptions}.
* @return {string} serialized value
* @api public
*/
declare function objectHash(object: any, options?: HashOptions): string;
/**
* Calculates the difference between two objects and returns a list of differences.
*
* @param {any} obj1 - The first object to compare.
* @param {any} obj2 - The second object to compare.
* @param {HashOptions} [opts={}] - Configuration options for hashing the objects. See {@link HashOptions}.
* @returns {DiffEntry[]} An array with the differences between the two objects.
*/
declare function diff(obj1: any, obj2: any, opts?: HashOptions): DiffEntry[];
declare class DiffEntry {
key: string;
type: "changed" | "added" | "removed";
newValue: DiffHashedObject;
oldValue?: DiffHashedObject | undefined;
constructor(key: string, type: "changed" | "added" | "removed", newValue: DiffHashedObject, oldValue?: DiffHashedObject | undefined);
toString(): string;
toJSON(): string;
}
declare class DiffHashedObject {
key: string;
value: any;
hash?: string | undefined;
props?: Record<string, DiffHashedObject> | undefined;
constructor(key: string, value: any, hash?: string | undefined, props?: Record<string, DiffHashedObject> | undefined);
toString(): string;
toJSON(): string;
}
/**
* Compare two objects using reference equality and stable deep hashing.
* @param {any} object1 First object
* @param {any} object2 Second object
* @param {HashOptions} hashOptions. Configuration options for hashing the objects. See {@link HashOptions}.
* @return {boolean} true if equal and false if not
* @api public
*/
declare function isEqual(object1: any, object2: any, hashOptions?: HashOptions): boolean;
export { type HashOptions as H, diff as d, isEqual as i, objectHash as o };
@@ -0,0 +1,111 @@
interface HashOptions {
/**
* Function to determine if a key should be excluded from hashing.
* @optional
* @param key - The key to check for exclusion.
* @returns {boolean} - Returns true to exclude the key from hashing.
*/
excludeKeys?: ((key: string) => boolean) | undefined;
/**
* Specifies whether to exclude values from hashing, so that only the object keys are hashed.
* @optional
*/
excludeValues?: boolean | undefined;
/**
* Specifies whether to ignore objects of unknown type (not directly serialisable) when hashing.
* @optional
* @default false
*/
ignoreUnknown?: boolean | undefined;
/**
* A function that replaces values before they are hashed, which can be used to customise the hashing process.
* @optional
* @param value - The current value to be hashed.
* @returns {any} - The value to use for hashing instead.
*/
replacer?: ((value: any) => any) | undefined;
/**
* Specifies whether the 'name' property of functions should be taken into account when hashing.
* @optional
* @default false
*/
respectFunctionNames?: boolean | undefined;
/**
* Specifies whether properties of functions should be taken into account when hashing.
* @optional
* @default false
*/
respectFunctionProperties?: boolean | undefined;
/**
* Specifies whether to include type-specific properties such as prototype or constructor in the hash to distinguish between types.
* @optional
* @default false
*/
respectType?: boolean | undefined;
/**
* Specifies whether arrays should be sorted before hashing to ensure consistent order.
* @optional
* @default false
*/
unorderedArrays?: boolean | undefined;
/**
* Specifies whether Set and Map instances should be sorted by key before hashing to ensure consistent order.
* @optional
* @default true
*/
unorderedObjects?: boolean | undefined;
/**
* Specifies whether the elements of `Set' and keys of `Map' should be sorted before hashing to ensure consistent order.
* @optional
* @default false
*/
unorderedSets?: boolean | undefined;
}
/**
* Serialize any JS value into a stable, hashable string
* @param {object} object value to hash
* @param {HashOptions} options hashing options. See {@link HashOptions}.
* @return {string} serialized value
* @api public
*/
declare function objectHash(object: any, options?: HashOptions): string;
/**
* Calculates the difference between two objects and returns a list of differences.
*
* @param {any} obj1 - The first object to compare.
* @param {any} obj2 - The second object to compare.
* @param {HashOptions} [opts={}] - Configuration options for hashing the objects. See {@link HashOptions}.
* @returns {DiffEntry[]} An array with the differences between the two objects.
*/
declare function diff(obj1: any, obj2: any, opts?: HashOptions): DiffEntry[];
declare class DiffEntry {
key: string;
type: "changed" | "added" | "removed";
newValue: DiffHashedObject;
oldValue?: DiffHashedObject | undefined;
constructor(key: string, type: "changed" | "added" | "removed", newValue: DiffHashedObject, oldValue?: DiffHashedObject | undefined);
toString(): string;
toJSON(): string;
}
declare class DiffHashedObject {
key: string;
value: any;
hash?: string | undefined;
props?: Record<string, DiffHashedObject> | undefined;
constructor(key: string, value: any, hash?: string | undefined, props?: Record<string, DiffHashedObject> | undefined);
toString(): string;
toJSON(): string;
}
/**
* Compare two objects using reference equality and stable deep hashing.
* @param {any} object1 First object
* @param {any} object2 Second object
* @param {HashOptions} hashOptions. Configuration options for hashing the objects. See {@link HashOptions}.
* @return {boolean} true if equal and false if not
* @api public
*/
declare function isEqual(object1: any, object2: any, hashOptions?: HashOptions): boolean;
export { type HashOptions as H, diff as d, isEqual as i, objectHash as o };
@@ -0,0 +1,8 @@
'use strict';
const utils = require('./shared/ohash.BElz9Fxo.cjs');
exports.diff = utils.diff;
exports.isEqual = utils.isEqual;
@@ -0,0 +1 @@
export { d as diff, i as isEqual } from './shared/ohash.DTXTHv91.cjs';
@@ -0,0 +1 @@
export { d as diff, i as isEqual } from './shared/ohash.DTXTHv91.mjs';
@@ -0,0 +1 @@
export { d as diff, i as isEqual } from './shared/ohash.DTXTHv91.js';
@@ -0,0 +1 @@
export { d as diff, i as isEqual } from './shared/ohash.BvSMZzli.mjs';
@@ -0,0 +1,51 @@
{
"name": "ohash",
"version": "1.1.6",
"description": "Super fast hashing library based on murmurhash3 written in Vanilla JS",
"repository": "unjs/ohash",
"license": "MIT",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.mjs",
"types": "./dist/utils.d.ts",
"require": "./dist/utils.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"dev": "vitest dev",
"lint": "eslint . && prettier -c src test",
"lint:fix": "eslint --fix . && prettier -w src test",
"prepack": "unbuild",
"release": "pnpm test && changelogen --release --push --publish --publishTag 1x",
"benchmark": "node benchmark/object-hash.mjs",
"test": "pnpm lint && vitest run && pnpm typecheck",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.13.8",
"@vitest/coverage-v8": "^3.0.7",
"benchmark": "^2.1.4",
"changelogen": "^0.6.0",
"eslint": "^9.21.0",
"eslint-config-unjs": "^0.4.2",
"prettier": "^3.5.2",
"typescript": "^5.8.2",
"unbuild": "^3.5.0",
"vitest": "^3.0.7"
},
"packageManager": "pnpm@10.5.2"
}
+166
View File
@@ -0,0 +1,166 @@
{
"name": "shiki-magic-move",
"type": "module",
"version": "0.5.2",
"packageManager": "pnpm@9.15.0",
"description": "Smoothly animated code blocks with Shiki",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/shikijs/shiki-magic-move#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/shikijs/shiki-magic-move.git"
},
"bugs": "https://github.com/shikijs/shiki-magic-move/issues",
"keywords": [
"shiki",
"animations"
],
"sideEffects": false,
"exports": {
".": {
"import": "./dist/index.mjs"
},
"./core": {
"import": "./dist/core.mjs"
},
"./renderer": {
"import": "./dist/renderer.mjs"
},
"./types": {
"import": "./dist/types.mjs"
},
"./vue": {
"import": "./dist/vue.mjs"
},
"./react": {
"import": "./dist/react.mjs"
},
"./solid": {
"import": "./dist/solid.mjs"
},
"./svelte": {
"import": "./dist/svelte.mjs"
},
"./style.css": "./dist/style.css",
"./dist/*": "./dist/*"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"./vue": [
"./dist/vue.d.ts"
],
"./react": [
"./dist/react.d.ts"
],
"./svelte": [
"./dist/svelte.d.ts"
],
"./solid": [
"./dist/solid.d.ts"
],
"./core": [
"./dist/core.d.ts"
],
"*": [
"./dist/*",
"./dist/index.d.ts"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"dev": "nr play",
"lint": "eslint .",
"prepublishOnly": "nr build",
"release": "bumpp && npm publish",
"start": "esno src/index.ts",
"test": "vitest",
"typecheck": "vue-tsc --noEmit",
"prepare": "simple-git-hooks",
"play": "nr -C playground dev",
"play:build": "nr -C playground build"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"shiki": "^1.1.6",
"solid-js": "^1.9.1",
"svelte": "^5.0.0-0",
"vue": "^3.4.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"shiki": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
}
},
"dependencies": {
"diff-match-patch-es": "^0.1.1",
"ohash": "^1.1.4"
},
"devDependencies": {
"@antfu/eslint-config": "^3.11.2",
"@antfu/ni": "^0.23.1",
"@antfu/utils": "^0.7.10",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@eslint-react/eslint-plugin": "^1.18.0",
"@rollup/plugin-babel": "^6.0.4",
"@sveltejs/vite-plugin-svelte": "^5.0.1",
"@types/diff-match-patch": "^1.0.36",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"@unocss/reset": "^0.65.1",
"@vueuse/core": "^12.0.0",
"babel-preset-solid": "^1.9.3",
"bumpp": "^9.9.0",
"eslint": "^9.16.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"eslint-plugin-solid": "^0.14.4",
"eslint-plugin-svelte": "^2.46.1",
"esno": "^4.8.0",
"lint-staged": "^15.2.10",
"pnpm": "^9.15.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rimraf": "^6.0.1",
"rollup": "^4.28.1",
"shiki": "^1.24.1",
"simple-git-hooks": "^2.11.1",
"solid-js": "^1.9.3",
"svelte": "^5.2.6",
"typescript": "^5.7.2",
"unbuild": "^2.0.0",
"unocss": "^0.65.1",
"vite": "^6.0.3",
"vitest": "^2.1.8",
"vue": "^3.5.13",
"vue-tsc": "^2.1.10"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}