Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce shiki code themes #888

Merged
merged 8 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
559 changes: 559 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@auth/sveltekit": "^1.7.4",
"@iconify/svelte": "^4.0.2",
"@iktakahiro/markdown-it-katex": "^4.0.1",
"@shikijs/markdown-it": "^1.24.1",
"@skeletonlabs/skeleton": "^3.0.0-next.9",
"@skeletonlabs/skeleton-svelte": "^1.0.0-next.14",
"@supabase/supabase-js": "^2.46.1",
Expand Down Expand Up @@ -49,6 +50,7 @@
"prettier": "^3.4.0",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"shiki": "^1.24.1",
"svelte": "^5.2.8",
"svelte-check": "^4.1.0",
"tailwindcss": "^3.4.15",
Expand Down
5 changes: 5 additions & 0 deletions src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ html,
body {
@apply h-full overflow-hidden;
}

code,
pre {
@apply rounded-md border dark:border-primary-500;
}
1 change: 1 addition & 0 deletions src/lib/runes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const transitionKey = rune("");
export const layout = rune("expanded");
export const lightMode = rune("light");
export const currentTheme = rune("tutors");
export const currentCodeTheme = rune("monokai");

export const currentLo = rune<Lo | null>(null);
export const currentCourse = rune<Course | null>(null);
Expand Down
27 changes: 24 additions & 3 deletions src/lib/services/course.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { courseUrl, currentCourse, currentLo } from "$lib/runes";
import type { Lo, Course, Lab } from "$lib/services/models/lo-types";
import { courseUrl, currentCodeTheme, currentCourse, currentLo } from "$lib/runes";
import type { Lo, Course, Lab, Note } from "$lib/services/models/lo-types";
import { decorateCourseTree } from "./models/lo-tree";
import { LiveLab } from "./models/live-lab";
import type { CourseService } from "./types.svelte";
import { themeService } from "$lib/ui/themes/theme-controller.svelte";
import { markdownService } from "./markdown";

export const courseService: CourseService = {
courses: new Map<string, Course>(),
labs: new Map<string, LiveLab>(),
notes: new Map<string, Note>(),
courseUrl: "",

async getOrLoadCourse(courseId: string, fetchFunction: typeof fetch): Promise<Course> {
let course = this.courses.get(courseId);
let courseUrl = courseId;

function isValidURL(url: string) {
const urlPattern = /^(https?:\/\/)?([A-Za-z0-9.-]+\.[A-Za-z]{2,})(:[0-9]+)?(\/[A-Za-z0-9_.-]+)*(\/[A-Za-z0-9_.-]+\?[A-Za-z0-9_=-]+)?(#.*)?$/;
const urlPattern =
/^(https?:\/\/)?([A-Za-z0-9.-]+\.[A-Za-z]{2,})(:[0-9]+)?(\/[A-Za-z0-9_.-]+)*(\/[A-Za-z0-9_.-]+\?[A-Za-z0-9_=-]+)?(#.*)?$/;
return urlPattern.test(url);
}

Expand Down Expand Up @@ -75,6 +79,8 @@ export const courseService: CourseService = {
let liveLab = this.labs.get(labId);
if (!liveLab) {
const lab = course.loIndex.get(labId) as Lab;
themeService.initCodeTheme();
markdownService.convertLabToHtml(course, lab, currentCodeTheme.value);
liveLab = new LiveLab(course, lab, labId);
this.labs.set(labId, liveLab);
}
Expand All @@ -93,6 +99,21 @@ export const courseService: CourseService = {
const course = await this.readCourse(courseId, fetchFunction);
const lo = course.loIndex.get(loId);
if (lo) currentLo.value = lo;
if (lo?.type === "note") {
markdownService.convertNoteToHtml(lo as Note, currentCodeTheme.value);
this.notes.set(loId, lo as Note);
}
return lo!;
},

refreshAllLabs(codeTheme: string) {
for (const liveLab of this.labs.values()) {
markdownService.convertLabToHtml(liveLab.course, liveLab.lab, codeTheme);
liveLab.convertMdToHtml();
liveLab.refreshStep();
}
for (const note of this.notes.values()) {
markdownService.convertNoteToHtml(note, codeTheme);
}
}
};
56 changes: 56 additions & 0 deletions src/lib/services/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Course, Lab, Note, Lo } from "./models/lo-types";
import { convertMdToHtml } from "./models/markdown-utils";

export const markdownService = {
convertLabToHtml(course: Course, lab: Lab, theme: string) {
lab.summary = convertMdToHtml(lab.summary, theme);
const url = lab.route.replace(`/lab/${course.courseId}`, course.courseUrl);
lab?.los?.forEach((step) => {
if (course.courseUrl) {
step.contentMd = this.filter(step.contentMd, url);
}
step.contentHtml = convertMdToHtml(step.contentMd, theme);
step.parentLo = lab;
step.type = "step";
});
},

convertNoteToHtml(note: Note, theme: string) {
note.summary = convertMdToHtml(note.summary, theme);
note.contentHtml = convertMdToHtml(note.contentMd, theme);
},

convertLoToHtml(course: Course, lo: Lo, theme: string = "monokai") {
if (lo.type === "lab" || lo.type == "note") {
// convertLabToHtml(course, lo as Lab);
} else {
if (lo.summary) lo.summary = convertMdToHtml(lo.summary, theme);
let md = lo.contentMd;
if (md) {
if (course.courseUrl) {
const url = lo.route.replace(`/${lo.type}/${course.courseId}`, course.courseUrl);
md = this.filter(md, url);
}
lo.contentHtml = convertMdToHtml(md, theme);
}
}
},

replaceAll(str: string, find: string, replace: string) {
return str.replace(new RegExp(find, "g"), replace);
},

filter(src: string, url: string): string {
let filtered = this.replaceAll(src, "./img\\/", `img/`);
filtered = this.replaceAll(filtered, "img\\/", `https://${url}/img/`);
filtered = this.replaceAll(filtered, "./archives\\/", `archives/`);

//filtered = replaceAll(filtered, "archives\\/", `https://${url}/archives/`);
filtered = this.replaceAll(filtered, "(?<!/)archives\\/", `https://${url}/archives/`);

// filtered = replaceAll(filtered, "./archive\\/(?!refs)", `archive/`);
filtered = this.replaceAll(filtered, "(?<!/)archive\\/(?!refs)", `https://${url}/archive/`);
filtered = this.replaceAll(filtered, "\\]\\(\\#", `](https://${url}#/`);
return filtered;
}
};
3 changes: 3 additions & 0 deletions src/lib/services/models/live-lab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export class LiveLab {
this.steps = Array.from(this.chaptersHtml.keys());
}

refreshStep() {
this.content = this.chaptersHtml.get(this.currentChapterShortTitle)!;
}
refreshNav() {
//const number = this.autoNumber ? this.lab.shortTitle + ": " : "";

Expand Down
4 changes: 2 additions & 2 deletions src/lib/services/models/lo-tree.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isCompositeLo, type Course, type Lo, type Composite, type LoType, type Topic } from "./lo-types";
import { convertLoToHtml } from "./markdown-utils";
import {
allVideoLos,
crumbs,
Expand All @@ -12,6 +11,7 @@ import {
filterByType
} from "./lo-utils";
import { createCompanions, createWalls, initCalendar, loadPropertyFlags } from "./course-utils";
import { markdownService } from "../markdown";

export function decorateCourseTree(course: Course, courseId: string = "", courseUrl = "") {
// define course properties
Expand Down Expand Up @@ -56,7 +56,7 @@ export function decorateLoTree(course: Course, lo: Lo) {
lo.breadCrumbs = [];
crumbs(lo, lo.breadCrumbs);
// Convert summary and contentMd to html
convertLoToHtml(course, lo);
markdownService.convertLoToHtml(course, lo);

if (isCompositeLo(lo)) {
// if Lo is composite, recursively decorate all child los
Expand Down
145 changes: 84 additions & 61 deletions src/lib/services/models/markdown-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import hljs from "highlight.js";
import type { Lab, Lo } from "./lo-types";
// @ts-ignore
import MarkdownIt from "markdown-it";
// @ts-ignore
Expand All @@ -21,9 +19,85 @@ import mark from "markdown-it-mark";
import footnote from "markdown-it-footnote";
// @ts-ignore
import deflist from "markdown-it-deflist";
import type { Course } from "./lo-types";

const markdownIt: any = new MarkdownIt({
import { createHighlighterCoreSync } from "shiki/core";
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";

import js from "shiki/langs/javascript.mjs";
import ts from "shiki/langs/typescript.mjs";
import css from "shiki/langs/css.mjs";
import html from "shiki/langs/html.mjs";
import json from "shiki/langs/json.mjs";
import yaml from "shiki/langs/yaml.mjs";
import markdown from "shiki/langs/markdown.mjs";
import bash from "shiki/langs/bash.mjs";
import python from "shiki/langs/python.mjs";
import sql from "shiki/langs/sql.mjs";
import typescript from "shiki/langs/typescript.mjs";
import java from "shiki/langs/java.mjs";
import kotlin from "shiki/langs/kotlin.mjs";
import csharp from "shiki/langs/csharp.mjs";
import c from "shiki/langs/c.mjs";
import cpp from "shiki/langs/cpp.mjs";
import go from "shiki/langs/go.mjs";
import rust from "shiki/langs/rust.mjs";
import php from "shiki/langs/php.mjs";
import ruby from "shiki/langs/ruby.mjs";
import swift from "shiki/langs/swift.mjs";

import monokai from "shiki/themes/monokai.mjs";
import solarizedDark from "shiki/themes/solarized-dark.mjs";
import solarizedLight from "shiki/themes/solarized-light.mjs";
import nightOwl from "shiki/themes/night-owl.mjs";
import githubDark from "shiki/themes/github-dark.mjs";
import dracula from "shiki/themes/dracula.mjs";
import snazziLight from "shiki/themes/snazzy-light.mjs";
import githubLightHighContrast from "shiki/themes/github-light-high-contrast.mjs";
const languages = [
js,
ts,
css,
html,
json,
yaml,
markdown,
bash,
python,
sql,
typescript,
java,
kotlin,
csharp,
c,
cpp,
go,
rust,
php,
ruby,
swift,
html
];

export const codeThemes = [
monokai,
solarizedLight,
githubDark,
githubLightHighContrast,
nightOwl,
dracula,
solarizedDark,
snazziLight
];

const shiki = createHighlighterCoreSync({
themes: codeThemes,
langs: languages,
engine: createJavaScriptRegexEngine()
});

let currentTheme = "monokai";

export const markdownIt = new MarkdownIt({
html: true, // Enable HTML tags in source
xhtmlOut: false, // Use '/' to close single tags (<br />).
breaks: false, // Convert '\n' in paragraphs into <br>
Expand All @@ -32,16 +106,11 @@ const markdownIt: any = new MarkdownIt({
typographer: true,
quotes: "“”‘’",
highlight: function (str: string, lang: string) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
"</code></pre>"
);
} catch (__) {}
try {
return shiki?.codeToHtml(str, { lang, theme: currentTheme });
} catch (e) {
return shiki?.codeToHtml(str, { lang: "yaml", theme: currentTheme });
}
return '<pre class="hljs"><code>' + markdownIt.utils.escapeHtml(str) + "</code></pre>";
}
});

Expand Down Expand Up @@ -85,53 +154,7 @@ markdownIt.renderer.rules.link_open = function (tokens: any, idx: any, options:
return defaultRender(tokens, idx, options, env, self);
};

function replaceAll(str: string, find: string, replace: string) {
return str.replace(new RegExp(find, "g"), replace);
}

function filter(src: string, url: string): string {
let filtered = replaceAll(src, "./img\\/", `img/`);
filtered = replaceAll(filtered, "img\\/", `https://${url}/img/`);
filtered = replaceAll(filtered, "./archives\\/", `archives/`);

//filtered = replaceAll(filtered, "archives\\/", `https://${url}/archives/`);
filtered = replaceAll(filtered, "(?<!/)archives\\/", `https://${url}/archives/`);

// filtered = replaceAll(filtered, "./archive\\/(?!refs)", `archive/`);
filtered = replaceAll(filtered, "(?<!/)archive\\/(?!refs)", `https://${url}/archive/`);
filtered = replaceAll(filtered, "\\]\\(\\#", `](https://${url}#/`);
return filtered;
}

export function convertLabToHtml(course: Course, lab: Lab) {
lab.summary = markdownIt.render(lab.summary);
const url = lab.route.replace(`/lab/${course.courseId}`, course.courseUrl);
lab.los.forEach((step) => {
if (course.courseUrl) {
step.contentMd = filter(step.contentMd, url);
}
step.contentHtml = markdownIt.render(step.contentMd);
step.parentLo = lab;
step.type = "step";
});
}

export function convertLoToHtml(course: Course, lo: Lo) {
if (lo.type === "lab") {
convertLabToHtml(course, lo as Lab);
} else {
if (lo.summary) lo.summary = markdownIt.render(lo.summary);
let md = lo.contentMd;
if (md) {
if (course.courseUrl) {
const url = lo.route.replace(`/${lo.type}/${course.courseId}`, course.courseUrl);
md = filter(md, url);
}
lo.contentHtml = markdownIt.render(md);
}
}
}

export function convertMdToHtml(md: string): string {
export function convertMdToHtml(md: string, codeTheme: string): string {
currentTheme = codeTheme;
return markdownIt.render(md);
}
4 changes: 3 additions & 1 deletion src/lib/services/types.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LiveLab } from "./models/live-lab";
import type { Course, IconType, Lo } from "./models/lo-types";
import type { Course, IconType, Lo, Note } from "./models/lo-types";

export type TutorsId = {
name: string;
Expand Down Expand Up @@ -60,6 +60,7 @@ export interface CardDetails {
export interface CourseService {
courses: Map<string, Course>;
labs: Map<string, LiveLab>;
notes: Map<string, Note>;
courseUrl: "";

getOrLoadCourse(courseId: string, fetchFunction: typeof fetch): Promise<Course>;
Expand All @@ -68,6 +69,7 @@ export interface CourseService {
readLab(courseId: string, labId: string, fetchFunction: typeof fetch): Promise<LiveLab>;
readWall(courseId: string, type: string, fetchFunction: typeof fetch): Promise<Lo[]>;
readLo(courseId: string, loId: string, fetchFunction: typeof fetch): Promise<Lo>;
refreshAllLabs(codeTheme: string): void;
}

export interface ProfileStore {
Expand Down
5 changes: 4 additions & 1 deletion src/lib/ui/learning-objects/content/Lab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { LiveLab } from "$lib/services/models/live-lab";
import { fly } from "svelte/transition";
import { slideFromLeft } from "$lib/ui/themes/animations";
import { currentCodeTheme } from "$lib/runes";

interface Props {
lab: LiveLab;
Expand Down Expand Up @@ -80,7 +81,9 @@
</div>
<div id="lab-panel" class="min-h-screen flex-1">
<article class="prose mr-4 max-w-none dark:prose-invert prose-pre:max-w-[70vw]">
{@html lab.content}
{#key currentCodeTheme.value}
{@html lab.content}
{/key}
</article>
</div>
</div>
Expand Down
Loading
Loading