195 lines
7.4 KiB
TypeScript
195 lines
7.4 KiB
TypeScript
import * as React from "react";
|
|
import { useState } from "react";
|
|
import * as ReactDOM from "react-dom"
|
|
import { useFetch } from "react-async";
|
|
|
|
const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });
|
|
|
|
type Language = {
|
|
name: string;
|
|
class: string;
|
|
embeddable: boolean;
|
|
standalone: boolean;
|
|
}
|
|
|
|
type LanguageSelectorProps = {
|
|
language: string;
|
|
onSelect(language: Language): void;
|
|
}
|
|
|
|
const LanguageSelector = ({ language, onSelect }: LanguageSelectorProps) => {
|
|
const { data: languages, isPending } = useFetch<Language[]>("/languages", { headers: { "Accept": "application/json" }});
|
|
|
|
const regex = language && new RegExp(`${language}`);
|
|
const matching = languages && languages.filter(current => !language || current.name.match(regex));
|
|
const isValidLanguage = languages && typeof languages.find(current => current.name === language) !== "undefined";
|
|
|
|
return isPending ? null : <ul className="demo__languages">
|
|
{ (isValidLanguage ? languages : matching).map(language => <li key={ language.name }>
|
|
<button type="button" className="btn btn-outline-primary btn-sm" onClick={ () => onSelect(language) }>
|
|
{ language.name }
|
|
</button>
|
|
</li>) }
|
|
</ul>
|
|
};
|
|
|
|
type HighlightFormProps = {
|
|
onSubmit(code: string, language: string);
|
|
}
|
|
|
|
const HighlightForm = ({ onSubmit }: HighlightFormProps) => {
|
|
const [code, setCode] = useState<string>("");
|
|
const [language, setLanguage] = useState<string>("");
|
|
|
|
const handleReset = () => {
|
|
setCode("");
|
|
setLanguage("");
|
|
};
|
|
|
|
const handleSubmit = () => onSubmit && onSubmit(code, language);
|
|
|
|
const handleLanguageSelection = language => setLanguage(language.name);
|
|
|
|
return (
|
|
<form className="demo__form">
|
|
<div className="form-group">
|
|
<label htmlFor="form_source">
|
|
<span className="far fa-code" aria-hidden="true"/> try it
|
|
</label>
|
|
<textarea name="source" className="form-control" id="form_source" rows={ 15 }
|
|
onChange={ ev => setCode((ev.target as HTMLTextAreaElement).value) }
|
|
value={ code }
|
|
/>
|
|
</div>
|
|
<div className="form-group row">
|
|
<label htmlFor="form_language" className="col-md-2 col-form-label">
|
|
<span className="far fa-lightbulb" aria-hidden="true"/> language
|
|
</label>
|
|
<div className="col-md-10">
|
|
<input type="text" name="language" id="form_language" className="form-control" required
|
|
value={ language }
|
|
onChange={ ev => setLanguage((ev.target as HTMLInputElement).value) }
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<LanguageSelector language={ language } onSelect={ handleLanguageSelection }/>
|
|
</div>
|
|
<div className="demo__actions">
|
|
<button type="button" className="btn btn-sm btn-outline-danger" onClick={ handleReset }>
|
|
<span className="far fa-trash fa-fw" aria-hidden="true"/> reset
|
|
</button>
|
|
<button type="button" className="btn btn-sm btn-outline-primary" onClick={ handleSubmit }>
|
|
<span className="far fa-code fa-fw" aria-hidden="true"/> highlight
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
};
|
|
|
|
const Loading = () => <div className="loading">
|
|
<span className="fad fa-spinner-third fa-spin fa-10x" />
|
|
</div>;
|
|
|
|
type HighlightedCodeProps = {
|
|
highlighted: HighlightedResponse;
|
|
onClose?(): void;
|
|
}
|
|
|
|
const tooltip = text => ({
|
|
'data-toggle': 'tooltip',
|
|
'title': text,
|
|
ref: el => $(el).tooltip(),
|
|
});
|
|
|
|
const HighlightedCode = ({ highlighted, onClose }: HighlightedCodeProps) => {
|
|
const widths = {
|
|
tokenization: `${ highlighted.times.tokenization / highlighted.times.total * 100 }%`,
|
|
processing: `${ highlighted.times.processing / highlighted.times.total * 100 }%`,
|
|
formatting: `${ highlighted.times.formatting / highlighted.times.total * 100 }%`,
|
|
};
|
|
|
|
return <>
|
|
<pre className="keylighter" dangerouslySetInnerHTML={ { __html: highlighted.html } }/>
|
|
|
|
<div className="d-flex align-items-center">
|
|
<div className="demo__times">
|
|
<div className="demo__time-splits">
|
|
<div className="demo__time-split demo__time-split--tokenization"
|
|
style={ { width: widths.tokenization } }
|
|
{ ...tooltip(`Tokenization took ${formatter.format(highlighted.times.tokenization)}ms`) }
|
|
/>
|
|
<div className="demo__time-split demo__time-split--processing"
|
|
style={ { width: widths.processing } }
|
|
{ ...tooltip(`Processing took ${formatter.format(highlighted.times.processing)}ms`) }
|
|
/>
|
|
<div className="demo__time-split demo__time-split--formatting"
|
|
style={ { width: widths.formatting } }
|
|
{ ...tooltip(`Formatting took ${formatter.format(highlighted.times.processing)}ms`) }
|
|
/>
|
|
</div>
|
|
<div className="demo__time-total">
|
|
Total time: { formatter.format(highlighted.times.total) }ms
|
|
</div>
|
|
</div>
|
|
<div className="demo__actions ml-auto">
|
|
<button type="button" className="btn btn-sm btn-outline-primary" onClick={ () => onClose && onClose() }>
|
|
<span className="far fa-sync fa-fw mr-1" aria-hidden="true"/>
|
|
try another
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>;
|
|
};
|
|
|
|
type DemoState = "form" | "submitting" | "show";
|
|
type HighlightedResponse = {
|
|
html: string;
|
|
times: {
|
|
total: number;
|
|
tokenization: number;
|
|
processing: number;
|
|
formatting: number;
|
|
}
|
|
}
|
|
|
|
const KeylighterDemo = props => {
|
|
const [state, setState] = useState<DemoState>("form");
|
|
const [highlighted, setHighlighted] = useState<HighlightedResponse>(null);
|
|
|
|
const handleCodeHighlight = async (source, language) => {
|
|
setState("submitting");
|
|
setHighlighted(await fetch('/highlight', {
|
|
credentials: "include",
|
|
method: "POST",
|
|
body: JSON.stringify({ source, language }),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}).then(res => res.json()));
|
|
setState("show");
|
|
};
|
|
|
|
return (
|
|
<div id="try-form" className="demo">
|
|
<div className="container">
|
|
{ state === "form" && <HighlightForm onSubmit={ handleCodeHighlight }/> }
|
|
{ state === "submitting" && <Loading /> }
|
|
{ state === "show" && <HighlightedCode highlighted={ highlighted } onClose={ () => setState("form") } /> }
|
|
</div>
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export function replace<T extends Element>(component: any, element: HTMLElement, callback?: () => void) {
|
|
const parent = document.createElement('div');
|
|
|
|
ReactDOM.render(component, parent, () => {
|
|
element.replaceWith(...Array.from(parent.childNodes));
|
|
callback && callback();
|
|
});
|
|
}
|
|
|
|
const root = document.getElementById('try-form');
|
|
replace(<KeylighterDemo />, root);
|