Skip to content

Commit

Permalink
Merge pull request #37 from flakestry/dev/use-typescript
Browse files Browse the repository at this point in the history
frontend: replace javascript with typescript
  • Loading branch information
domenkozar committed Nov 6, 2023
2 parents 2682fe3 + 323a673 commit b29e7c0
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 30 deletions.
2 changes: 2 additions & 0 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ in
npm.install.enable = true;
};

languages.typescript.enable = true;

languages.elm.enable = true;

services.opensearch.enable = !config.container.isBuilding;
Expand Down
27 changes: 21 additions & 6 deletions frontend/src/interop.js → frontend/src/interop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,35 @@ import Clipboard from 'clipboard';
import './index.css';
import './web-components/highlight-code';

// Types for Elm Land interop functions
namespace ElmLand {
export type FlagsFunction =
({ env }: { env: Record<string, string> }) => unknown

export type OnReadyFunction = ({ env, app }: {
env: Record<string, string>,
app: { ports?: Record<string, Port> }
}) => void

export type Port = {
subscribe?: (callback: (data: unknown) => void) => void,
unsubscribe?: (callback: (data: unknown) => void) => void,
send?: (data: unknown) => void
}
}

// This is called BEFORE your Elm app starts up
//
// The value returned here will be passed as flags
//
// The value returned here will be passed as flags
// into your `Shared.init` function.
export const flags = ({ env }) => {
return {}
}
export const flags: ElmLand.FlagsFunction = () => { }

// This is called AFTER your Elm app starts up
//
// Here you can work with `app.ports` to send messages
// to your Elm application, or subscribe to incoming
// messages from Elm
export const onReady = ({ app, env }) => {
export const onReady: ElmLand.OnReadyFunction = () => {
const clipboard = new Clipboard('.clipboard');

clipboard.on('success', function(e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import DOMPurify from 'dompurify';
import hljs from 'highlight.js';

window.customElements.define('highlight-code', class extends HTMLElement {
// Cached instances
private _marked: Marked | undefined;
private _DOMPurify: DOMPurify.DOMPurifyI | undefined;

constructor() { super(); }

connectedCallback() { this.render(); }

render() {
const code = this.getAttribute('code');
const code = this.getAttribute('code') ?? '';
const lang = this.getAttribute('language') ?? 'markdown';
const baseUrl = this.getAttribute('baseUrl') ?? '';
const rawBaseUrl = this.getAttribute('rawBaseUrl') ?? '';
Expand All @@ -25,46 +29,43 @@ window.customElements.define('highlight-code', class extends HTMLElement {
}
}

parseCode(code, lang) {
parseCode(code: string, lang: string) {
const sanitized = DOMPurify.sanitize(this.highlightCode(code, lang));

this.classList.add('block', 'whitespace-pre', 'hljs', `language-${lang}`);
this.innerHTML = sanitized;
}

parseMarkdown(markdown, baseUrl = '', rawBaseUrl = '') {
async parseMarkdown(markdown: string, baseUrl = '', rawBaseUrl = '') {
if (baseUrl) {
this.DOMPurify.addHook('afterSanitizeAttributes', function(node) {
if (node.hasAttribute('href')) {
const url = new URL(
removeRootPath(node.getAttribute('href')),
baseUrl,
);
node.setAttribute('href', url.toString());
let href = node.getAttribute('href');
if (href !== null) {
const newHref = new URL(removeRootPath(href), baseUrl);
node.setAttribute('href', newHref.toString());
}

if (node.hasAttribute('src')) {
const src = new URL(
removeRootPath(node.getAttribute('src')),
rawBaseUrl || baseUrl,
);
node.setAttribute('src', src.toString());
let src = node.getAttribute('src');
if (src !== null) {
const newSrc = new URL(removeRootPath(src), rawBaseUrl || baseUrl);
node.setAttribute('src', newSrc.toString());
}
});
}

const sanitized = this.DOMPurify.sanitize(this.marked.parse(markdown));
const parsed = await this.marked.parse(markdown);
const sanitized = this.DOMPurify.sanitize(parsed);

this.classList.add('block');
this.innerHTML = sanitized;
}

highlightCode(code, lang) {
highlightCode(code: string, lang: string): string {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language, ignoreIllegals: true }).value;
}

get marked() {
get marked(): Marked {
if (this._marked) {
return this._marked;
}
Expand All @@ -80,7 +81,7 @@ window.customElements.define('highlight-code', class extends HTMLElement {
return this._marked;
}

get DOMPurify() {
get DOMPurify(): DOMPurify.DOMPurifyI {
if (this._DOMPurify) {
return this._DOMPurify;
}
Expand All @@ -91,7 +92,7 @@ window.customElements.define('highlight-code', class extends HTMLElement {
}
})

function removeRootPath(path = '') {
function removeRootPath(path = ''): string {
if (path.startsWith('/')) {
return path.substring(1);
}
Expand Down
13 changes: 13 additions & 0 deletions frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"files": [
"src/interop.ts"
],
"compilerOptions": {
"lib": ["es6", "dom"],

"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
}
}
38 changes: 34 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/dompurify": "^3.0.4",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
"elm-land": "*",
"github-markdown-css": "^5.4.0",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"tailwindcss": "^3.2.4",
"typescript": "^5.2.2",
"vite": "^4.4.9"
}
}

0 comments on commit b29e7c0

Please sign in to comment.