Skip to content
Snippets Groups Projects
Commit 55b78cb4 authored by Linus Keiser's avatar Linus Keiser :speech_balloon:
Browse files

--wip--

parent 198e986f
No related branches found
No related tags found
No related merge requests found
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
}}>Init</button }}>Init</button
> >
<main> <CodeBlock code={data.submission.code} />
<CodeBlock code={data.submission.code} />
</main>
<style> <style>
</style> </style>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
// Themes // Themes
import themeDarkPlus from 'shiki/themes/dark-plus.mjs'; import themeDarkPlus from 'shiki/themes/dark-plus.mjs';
import themeVitesseLight from 'shiki/themes/vitesse-light.mjs';
// Languages // Languages
import haskell from 'shiki/langs/haskell.mjs'; import haskell from 'shiki/langs/haskell.mjs';
import python from 'shiki/langs/python.mjs'; import python from 'shiki/langs/python.mjs';
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
const shiki = createHighlighterCoreSync({ const shiki = createHighlighterCoreSync({
engine: createJavaScriptRegexEngine(), engine: createJavaScriptRegexEngine(),
themes: [themeDarkPlus], themes: [themeDarkPlus, themeVitesseLight],
langs: [haskell, python, markdown] langs: [haskell, python, markdown]
}); });
</script> </script>
...@@ -19,11 +20,12 @@ ...@@ -19,11 +20,12 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import type { CodeBlockProps } from './types'; import type { CodeBlockProps } from './types';
import { sourceMappingTransformer } from './transformers';
let { let {
code = '', code = '',
lang = 'markdown', lang = 'markdown',
theme = 'dark-plus', theme = 'vitesse-light',
// Base Style Props // Base Style Props
base = ' overflow-hidden', base = ' overflow-hidden',
rounded = 'rounded-container', rounded = 'rounded-container',
...@@ -36,11 +38,19 @@ ...@@ -36,11 +38,19 @@
}: CodeBlockProps = $props(); }: CodeBlockProps = $props();
const generatedHtml = shiki.codeToHtml(code, { const generatedHtml = shiki.codeToHtml(code, {
structure: 'classic', // one span per line, one span per token in a line
lang, lang,
theme, theme,
//meta: //meta:
//transformers: [sourceMappingTransformer], transformers: [sourceMappingTransformer],
structure: 'classic' // one span per line, one span per token in a line decorations: [
{
start: 0,
end: 36,
properties: { class: 'bg-amber-200' }
// alwaysWrap: true
}
]
}); });
let codeBlockEl: HTMLElement; let codeBlockEl: HTMLElement;
...@@ -78,4 +88,7 @@ ...@@ -78,4 +88,7 @@
</div> </div>
<style> <style>
.highlighted-word {
background-color: rgba(255, 255, 128, 0.2);
}
</style> </style>
import type { CodeToHastOptions, ShikiTransformer, ThemedToken } from 'shiki'; import type { CodeToHastOptions, ShikiTransformer, ThemedToken } from 'shiki';
import type * as Hast from 'hast'; import type * as Hast from 'hast';
import type { SelectionState, Position, Range } from './types';
/*
Decorations
We provide a decorations API allowing you to wrap custom classes and attributes around ranges of your code.
```
import { codeToHtml } from 'shiki'
const code = `
const x = 10
console.log(x)
`.trim()
const html = await codeToHtml(code, {
theme: 'vitesse-light',
lang: 'ts',
decorations: [
{
// line and character are 0-indexed
start: { line: 1, character: 0 },
end: { line: 1, character: 11 },
properties: { class: 'highlighted-word' }
}
]
})
```
The positions can also be 0-indexed offsets relative to the code:
```
const html = await codeToHtml(code, {
theme: 'vitesse-light',
lang: 'ts',
decorations: [
{
start: 21,
end: 24,
properties: { class: 'highlighted-word' }
}
]
})
```
# Use Decorations in Transformers
For advanced use cases, you can use the Transformers API to have full access to the tokens and the HAST tree.
Meanwhile, if you want to append decorations within a transformer, you can do that with:
```
import { codeToHtml, ShikiTransformer } from 'shiki'
const myTransformer: ShikiTransformer = {
name: 'my-transformer',
preprocess(code, options) {
// Generate the decorations somehow
const decorations = doSomethingWithCode(code)
// Make sure the decorations array exists
options.decorations ||= []
// Append the decorations
options.decorations.push(...decorations)
}
}
const html = await codeToHtml(code, {
theme: 'vitesse-light',
lang: 'ts',
transformers: [
myTransformer
]
})
```
Note that you can only provide decorations in or before the preprocess hook. In later hooks, changes to the decorations arrary will be ignored.
*/
export const sourceMappingTransformer: ShikiTransformer = { export const sourceMappingTransformer: ShikiTransformer = {
name: 'source-mapping-transformer', name: 'source-mapping-transformer',
// Called before the code is tokenized // Called before the code is tokenized
...@@ -89,10 +15,16 @@ export const sourceMappingTransformer: ShikiTransformer = { ...@@ -89,10 +15,16 @@ export const sourceMappingTransformer: ShikiTransformer = {
col: number, col: number,
lineElement: Hast.Element, lineElement: Hast.Element,
token: ThemedToken token: ThemedToken
) {}, ) {
lineElement.properties['data-line'] = line;
spanElement.properties['data-column'] = col;
spanElement.properties['data-src-offset'] = token.offset;
spanElement.properties['data-content-length'] = token.content.length;
},
// Called for each line <span> tag // Called for each line <span> tag
line(lineElement: Hast.Element, line: number) { line(lineElement: Hast.Element, line: number) {
lineElement.properties['data-source-line'] = line; // lineElement.properties['data-source-line'] = line;
}, },
// Called for each <code> tag, wraps all the lines // Called for each <code> tag, wraps all the lines
// Returning a new Node will replace the original one. // Returning a new Node will replace the original one.
...@@ -108,31 +40,55 @@ export const sourceMappingTransformer: ShikiTransformer = { ...@@ -108,31 +40,55 @@ export const sourceMappingTransformer: ShikiTransformer = {
postprocess(html: string, options) {} postprocess(html: string, options) {}
}; };
// export function domSelectionToSourceSelection( /**
// selection: Selection, * Map a DOM selection to a source selection
// codeEl: HTMLElement *
// ): SourceSelection | null { * Requires that the element we inspect has been tagged with the neccessary metadata.
// if (!selection || selection.rangeCount === 0 || !codeEl) return null; * @param selection DOM selection
// const range = selection.getRangeAt(0); * @param shikiHtml HTLM generated by shiki, tagged with source-mapping metadata
*/
// if (!codeEl.contains(range.commonAncestorContainer)) return null; export function domSelectionToSourceRange(
selection: Selection,
// const startPosition = findSourcePosition(range.startContainer, range.startOffset, codeEl); shikiHtml: HTMLElement
// const endPosition = findSourcePosition(range.endContainer, range.endOffset, codeEl); ): SelectionState | null {
// TODO check that the selection is within shikiHtml
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const startPosition = findSourcePosition(range.startContainer, range.startOffset, shikiHtml);
const endPosition = findSourcePosition(range.endContainer, range.endOffset, shikiHtml);
if (!startPosition || !endPosition) return null;
// const selectedText = selection.toString(); // TODO get source text, not DOM text
return {
type: selection.type.toLowerCase() as any,
anchor: startPosition,
focus: endPosition,
direction: selection.direction as any,
isCollapsed: selection.isCollapsed,
timestamp: Date.now()
};
}
// if (!startPosition || !endPosition) return null; /**
@param node
@param offset Number of characters (0-indexed) that the selection is offset within the node (for text, CDATASection, and Comment nodes)
@param element Element containing the node
*/
function findSourcePosition(node: Node, offset: number, element: HTMLElement): Position | null {
// TODO handle other node types?
if (node.nodeType !== Node.TEXT_NODE) return null;
// const selectedText = selection.toString(); // TODO get source text, not DOM text // NOTE: we assume that this is ALWAYS a tokens <span>
const parent = node.parentElement;
// const result = sourceSelection.safeParse({ if (!parent) return null;
// start: startPosition,
// end: endPosition,
// text: selectedText,
// type: selection.type
// });
// return result.success ? result.data : null; const tokenOffset = parseInt(parent.getAttribute('data-token-offset') || '0', 10);
// } const tokenLen = parseInt(parent.getAttribute('data-token-len') || '0', 10);
}
// function findSourcePosition( // function findSourcePosition(
// node: Node, // node: Node,
......
...@@ -16,7 +16,7 @@ export interface CodeBlockProps { ...@@ -16,7 +16,7 @@ export interface CodeBlockProps {
} }
// Mapped from a DOM `Selection`, where anchor and focus are `Node`s, to a source selection // Mapped from a DOM `Selection`, where anchor and focus are `Node`s, to a source selection
interface SelectionState { export interface SelectionState {
type: 'caret' | 'range'; // depending on whether the caret is placed at a single point in the text, or a range has been selected type: 'caret' | 'range'; // depending on whether the caret is placed at a single point in the text, or a range has been selected
anchor: Position; // MAPPED from Selection.anchorNode/anchorOffset - Starting position (may be after focus) anchor: Position; // MAPPED from Selection.anchorNode/anchorOffset - Starting position (may be after focus)
focus: Position; // MAPPED from Selection.focusNode/anchorOffset - Ending position (may be before anchor) focus: Position; // MAPPED from Selection.focusNode/anchorOffset - Ending position (may be before anchor)
...@@ -25,14 +25,19 @@ interface SelectionState { ...@@ -25,14 +25,19 @@ interface SelectionState {
timestamp: number; // For selection history tracking timestamp: number; // For selection history tracking
} }
export interface Range {
start: Position;
end: Position;
}
// Position in the source document // Position in the source document
interface Position { export interface Position {
line: number; // 1-based line number line: number; // 1-based line number
column: number; // 0-based column number column: number; // 0-based column number
offset: number; // Absolute character offset in source document offset: number; // Absolute character offset in source document
} }
interface DocumentLine { export interface DocumentLine {
id: string; // Stable line identifier id: string; // Stable line identifier
content: string; // Line content content: string; // Line content
lineNumber: number; // 1-based line number lineNumber: number; // 1-based line number
...@@ -45,9 +50,7 @@ interface DocumentLine { ...@@ -45,9 +50,7 @@ interface DocumentLine {
}; };
} }
type Annotation = DocumentAnnotation | LineAnnotation | RangeAnnotation; export interface BaseAnnotation {
interface BaseAnnotation {
type: 'document' | 'line' | 'range'; // Annotation scope type: 'document' | 'line' | 'range'; // Annotation scope
id: string; id: string;
content: string; // Structured annotation content content: string; // Structured annotation content
...@@ -59,14 +62,14 @@ interface BaseAnnotation { ...@@ -59,14 +62,14 @@ interface BaseAnnotation {
}; };
} }
interface DocumentAnnotation extends BaseAnnotation { export interface DocumentAnnotation extends BaseAnnotation {
type: 'document'; type: 'document';
visualSettings?: { visualSettings?: {
color?: string; color?: string;
}; };
} }
interface LineAnnotation extends BaseAnnotation { export interface LineAnnotation extends BaseAnnotation {
type: 'line'; type: 'line';
line: Position; // column of Position is 0, i.e. first on line line: Position; // column of Position is 0, i.e. first on line
visualSettings?: { visualSettings?: {
...@@ -75,7 +78,7 @@ interface LineAnnotation extends BaseAnnotation { ...@@ -75,7 +78,7 @@ interface LineAnnotation extends BaseAnnotation {
}; };
} }
interface RangeAnnotation extends BaseAnnotation { export interface RangeAnnotation extends BaseAnnotation {
type: 'range'; type: 'range';
range: { range: {
start: Position; start: Position;
...@@ -87,3 +90,5 @@ interface RangeAnnotation extends BaseAnnotation { ...@@ -87,3 +90,5 @@ interface RangeAnnotation extends BaseAnnotation {
priority?: number; priority?: number;
}; };
} }
export type Annotation = DocumentAnnotation | LineAnnotation | RangeAnnotation;
import { getDb } from '$lib/surreal.svelte'; import { getDb } from '$lib/surreal.svelte';
import { submissionSchema, type Submission, type SubmissionCreate } from '$lib/surreal/schema'; import {
evaluationSchema,
submissionSchema,
type Submission,
type SubmissionCreate
} from '$lib/surreal/schema';
import { RecordId, StringRecordId } from 'surrealdb'; import { RecordId, StringRecordId } from 'surrealdb';
export const db = await getDb(); export const db = await getDb();
...@@ -7,12 +12,12 @@ export const db = await getDb(); ...@@ -7,12 +12,12 @@ export const db = await getDb();
const dbgSubId = new RecordId('submission', 'dbg1'); const dbgSubId = new RecordId('submission', 'dbg1');
const dbgEvalId = new RecordId('evaluation', 'dbg1'); const dbgEvalId = new RecordId('evaluation', 'dbg1');
export async function getSubmission(): Submission { export async function getSubmission() {
return submissionSchema.parse(await db.select(dbgSubId)); return submissionSchema.parse(await db.select(dbgSubId));
} }
export async function getEvaluation(): Evaluation { export async function getEvaluation() {
return await db.select(dbgEvalId); return evaluationSchema.parse(await db.select(dbgEvalId));
} }
export async function initDbgData() { export async function initDbgData() {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment