Skip to content

Commit

Permalink
Merge pull request #888 from tutors-sdk/features/code-style
Browse files Browse the repository at this point in the history
introduce shiki code themes
  • Loading branch information
edeleastar authored Dec 12, 2024
2 parents b003de4 + e495960 commit b511c86
Show file tree
Hide file tree
Showing 16 changed files with 789 additions and 77 deletions.
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

0 comments on commit b511c86

Please sign in to comment.