From b91466fcdaa06d5007a56e5395faacd6e135c9f0 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 13 Apr 2020 17:00:16 +0200 Subject: [PATCH] Add dybanamic demo form using preact --- assets/sass/_demo.scss | 45 ++++++++ assets/sass/style.scss | 12 ++ assets/ts/app.ts | 30 ++--- assets/ts/form.ts | 115 ------------------- assets/ts/try-form.tsx | 126 +++++++++++++++++++++ config/services.yaml | 18 +-- package.json | 1 + src/Controller/DemoController.php | 25 ---- src/Controller/HighlightAction.php | 63 +++++++++++ src/Listener/KeyLighterVersionListener.php | 5 - templates/keylighter.html.twig | 8 +- tsconfig.json | 4 +- yarn.lock | 5 + 13 files changed, 275 insertions(+), 182 deletions(-) create mode 100644 assets/sass/_demo.scss delete mode 100755 assets/ts/form.ts create mode 100644 assets/ts/try-form.tsx delete mode 100755 src/Controller/DemoController.php create mode 100755 src/Controller/HighlightAction.php diff --git a/assets/sass/_demo.scss b/assets/sass/_demo.scss new file mode 100644 index 0000000..050d421 --- /dev/null +++ b/assets/sass/_demo.scss @@ -0,0 +1,45 @@ +.demo__actions { + display: flex; + justify-content: flex-end; + + .btn { + font-weight: 600; + } + + > *:not(:last-child) { + margin-right: .5rem; + } +} + +.demo__times { + flex: 1 1 auto; + margin-right: 1rem; +} + +.demo__time-total { + font-size: .8rem; + font-weight: 700; + margin-top: 0.25rem; +} + +.demo__time-splits { + border-radius: 0; + height: .4rem; + display: flex; +} + +.demo__time-split { + height: 100%; +} + +.demo__time-split--tokenization { + background: $color-accent; +} + +.demo__time-split--processing { + background: darken($color-accent, 10%); +} + +.demo__time-split--formatting { + background: darken($color-accent, 20%); +} diff --git a/assets/sass/style.scss b/assets/sass/style.scss index d41f198..6aff7d6 100755 --- a/assets/sass/style.scss +++ b/assets/sass/style.scss @@ -6,6 +6,7 @@ @import "keylighter"; @import "sidebar"; @import "docs"; +@import "demo"; $footer-height: 60px; @@ -88,6 +89,10 @@ body { } } +[data-not-ready] { + display: none; +} + #try-form { padding: 1rem 0; color: $foreground; @@ -115,6 +120,13 @@ body { &.visible { display: block; } + + .loading { + color: $color-accent; + justify-content: center; + display: flex; + padding: 5rem; + } } .d-flex.dual { diff --git a/assets/ts/app.ts b/assets/ts/app.ts index 3d83626..03d3109 100644 --- a/assets/ts/app.ts +++ b/assets/ts/app.ts @@ -1,27 +1,15 @@ -import 'bootstrap' -import TryItForm from "./form" -import '@fortawesome/fontawesome-pro/css/all.min.css' +if (process.env.NODE_ENV === 'development') { + require("preact/debug"); +} +// dependencies +import 'bootstrap' + +// styles +import '@fortawesome/fontawesome-pro/css/all.min.css' import 'typeface-raleway' import '../sass/style.scss' $('[data-toggle="tooltip"]').tooltip(); -let $form = $('#try-form'); -let $toggle = $('#form-toggle'); - -let form = new TryItForm($form, $toggle); - -$toggle.click(() => { - form.toggle(); - return false; -}); - -let $edit = $('#snippet-edit').click(() => { - let $snippet = $('#snippet'); - - form.source = $snippet.data('source'); - form.language = $snippet.data('language'); - - form.show(); -}); +import './try-form' diff --git a/assets/ts/form.ts b/assets/ts/form.ts deleted file mode 100755 index 324db23..0000000 --- a/assets/ts/form.ts +++ /dev/null @@ -1,115 +0,0 @@ -export default class TryItForm { - private $form: JQuery; - private $toggle: JQuery; - private $picker: JQuery; - - constructor($form: JQuery, $toggle: JQuery) { - this.$form = $form; - this.$picker = $form.find('.languages'); - this.$picker.find('.language').click(e => this.pick(e)); - this.$form.find('[name="language"]').on('input', e => this.update(e)); - this.$toggle = $toggle; - } - - public toggle() { - if (this.$form.hasClass('visible')) { - this.hide(); - } else { - this.show(); - } - } - - public show() { - this.$form.slideDown({ - complete: () => this.$form.addClass('visible') - }); - this.$toggle - .addClass('visible') - .addClass('active'); - - return this; - } - - public hide() { - this.$form.slideUp({ - complete: () => this.$form.removeClass('visible') - }); - - this.$toggle - .removeClass('visible') - .removeClass('active'); - - return this; - } - - get source() { - return this.$form.find('[name="source"]').val(); - } - - set source(value: string) { - this.$form.find('[name="source"]').val(value); - } - - get language() { - return this.$form.find('[name="language"]').val(); - } - - set language(value: string) { - this.$form.find('[name="language"]').val(value); - } - - public set(source: string, language: string) { - this.source = source; - this.language = language; - } - - public submit() { - this.$form.find('form').submit(); - } - - private update(e: JQueryKeyEventObject) { - let $this = $(e.currentTarget); - let tokens = $this.val() - .split(/\b/) - .map(x => x.trim()) - .filter(x => !!x); - - let last: string|boolean = false; - - if (tokens.length) { - last = tokens[tokens.length - 1]; - if (last == '>') { - last = false; - } - } - - let standalone = tokens.indexOf('>') == -1; - this.$picker - .toggleClass('embeddable', !standalone) - .toggleClass('standalone', standalone); - - this.$picker.find('.language').each((index, elem) => { - let $elem = $(elem); - let show = !last || (typeof last == 'string' && $elem.text().indexOf(last) !== -1); - - $elem.toggleClass('hidden', !show); - }) - } - - private pick(e: JQueryEventObject) { - let $this = $(e.currentTarget); - let $selector = this.$form.find('[name="language"]'); - let input = $selector[0]; - let value = input.value; - - let start = input.selectionStart; - let end = input.selectionEnd; - - while(start > 0 && value[start-1] != ' ' && start--) {} - while(end < value.length && value[end] != ' ' && end++) {} - - input.value = value.substring(0, start) + $this.text().trim() + value.substring(end); - - return false; - } -} diff --git a/assets/ts/try-form.tsx b/assets/ts/try-form.tsx new file mode 100644 index 0000000..de0c5ea --- /dev/null +++ b/assets/ts/try-form.tsx @@ -0,0 +1,126 @@ +import { Fragment, h, render } from "preact"; +import { useState } from "preact/hooks"; + +const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }); + +type HighlightFormProps = { + onSubmit(code: string, language: string); +} + +const HighlightForm = ({ onSubmit }: HighlightFormProps) => { + const [code, setCode] = useState(""); + const [language, setLanguage] = useState(""); + + const handleReset = () => { + setCode(""); + setLanguage(""); + }; + + const handleSubmit = () => onSubmit && onSubmit(code, language); + + return ( +
+
+ + +
+
+ +
+ setLanguage((ev.target as HTMLInputElement).value) }/> +
+
+
+
+
+
+
+ + +
+
+ ) +}; + +const Loading = () =>
+ +
; + +type HighlightedCodeProps = { + highlighted: HighlightedResponse; + onClose?(): void; +} + +const HighlightedCode = ({ highlighted, onClose }: HighlightedCodeProps) => + +
+
+        
+
+
+
+
+
+
+
+ Total time: { formatter.format(highlighted.times.total) }ms +
+
+
+ +
+
+ ; + +type DemoState = "form" | "submitting" | "show"; +type HighlightedResponse = { + html: string; + times: { + total: number; + tokenization: number; + processing: number; + formatting: number; + } +} + +const KeylighterDemo = () => { + const [state, setState] = useState("form"); + const [highlighted, setHighlighted] = useState(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 ( +
+
+ { state === "form" && } + { state === "submitting" && } + { state === "show" && setState("form") } /> } +
+
+ ) +}; + +const root = document.getElementById('try-form'); +render(, root.parentElement, root); diff --git a/config/services.yaml b/config/services.yaml index 9f9fcc0..2f7723b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -27,13 +27,6 @@ services: resource: '../src/Service/*' public: true - # controllers are imported separately to make sure they're public - # and have a tag that allows actions to type-hint services - App\Controller\: - resource: '../src/Controller' - public: true - tags: ['controller.service_arguments'] - # controllers are imported separately to make sure they're public # and have a tag that allows actions to type-hint services App\Command\: @@ -43,7 +36,7 @@ services: App\Listener\KeyLighterVersionListener: tags: - - { name: kernel.event_listener, event: kernel.request } + - { name: kernel.event_listener, event: kernel.request, priority: 4096 } - { name: kernel.event_listener, event: kernel.response } App\MessageHandler\UpdateKeylighterHandler: @@ -55,6 +48,8 @@ services: Kadet\Highlighter\KeyLighter: factory: ['@App\Service\KeyLighterVersioner', 'getKeyLighter'] + Kadet\Highlighter\Formatter\HtmlFormatter: ~ + League\CommonMark\Environment: factory: ['League\CommonMark\Environment', 'createGFMEnvironment'] calls: @@ -67,3 +62,10 @@ services: twig.versioner: '@App\Service\KeyLighterVersioner' twig.keylighter: '@App\Twig\KeyLighterTwigAccess' + + # controllers are imported separately to make sure they're public + # and have a tag that allows actions to type-hint services + App\Controller\: + resource: '../src/Controller' + public: true + tags: ['controller.service_arguments'] diff --git a/package.json b/package.json index 26855ec..a9956f3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "build": "encore production --progress" }, "dependencies": { + "preact": "^10.4.0", "typeface-raleway": "^0.0.75" } } diff --git a/src/Controller/DemoController.php b/src/Controller/DemoController.php deleted file mode 100755 index 0a5527d..0000000 --- a/src/Controller/DemoController.php +++ /dev/null @@ -1,25 +0,0 @@ -setLanguage($request->get('language')); - $snippet->setCode($request->get('source')); - - return $this->forward(sprintf('%s::showAction', SnippetController::class), ['snippet' => $snippet]); - } - -} diff --git a/src/Controller/HighlightAction.php b/src/Controller/HighlightAction.php new file mode 100755 index 0000000..06e6f49 --- /dev/null +++ b/src/Controller/HighlightAction.php @@ -0,0 +1,63 @@ +keylighter = $keylighter; + $this->formatter = $formatter; + } + + /** + * @Route("/highlight", name="demo_highlight", methods={"POST"}) + */ + public function __invoke(Request $request) + { + $body = json_decode($request->getContent(), true); + + $language = $this->keylighter->languageByName($body['language']); + $source = $body['source']; + + $times = []; + + $tokens = $this->time(function () use ($source, $language) { + return $language->tokenize($source); + }, $times['tokenization']); + + $tokens = $this->time(function () use ($tokens, $language) { + return $language->parse($tokens); + }, $times['processing']); + + $formatted = $this->time(function () use ($tokens) { + return $this->formatter->format($tokens); + }, $times['formatting']); + + $times['total'] = array_sum($times); + + return new JsonResponse([ + 'html' => $formatted, + 'times' => array_map(function ($time) { + return $time * 1000; + }, $times), + ]); + } + + private function time(callable $call, &$time) { + $start = microtime(true); + $return = $call(); + $time = microtime(true) - $start; + + return $return; + } +} diff --git a/src/Listener/KeyLighterVersionListener.php b/src/Listener/KeyLighterVersionListener.php index 0aa70aa..1698d1f 100755 --- a/src/Listener/KeyLighterVersionListener.php +++ b/src/Listener/KeyLighterVersionListener.php @@ -13,11 +13,6 @@ class KeyLighterVersionListener { private $versioner; - /** - * KeyLighterVersionListener constructor. - * - * @param $versioner - */ public function __construct(KeyLighterVersioner $versioner) { $this->versioner = $versioner; diff --git a/templates/keylighter.html.twig b/templates/keylighter.html.twig index fa0741f..a0f16d7 100755 --- a/templates/keylighter.html.twig +++ b/templates/keylighter.html.twig @@ -1,6 +1,4 @@ {% import "macros.twig" as helper %} -{% set current = current is defined ? current : 'demo' %} -{% set showForm = showForm|default(false) %} @@ -38,11 +36,7 @@
-
-
- {% include 'demo/_form.html.twig' %} -
-
+
{% block content '' %} diff --git a/tsconfig.json b/tsconfig.json index 1759508..27c65c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,9 @@ "sourceMap": true, "noImplicitThis": true, "moduleResolution": "node", - "downlevelIteration": true + "downlevelIteration": true, + "jsx": "react", + "jsxFactory": "h" }, "files": ["assets/ts/app.ts"], "include": ["assets/ts/**/*.ts"] diff --git a/yarn.lock b/yarn.lock index f6d5891..509306c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5351,6 +5351,11 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.5 source-map "^0.6.1" supports-color "^6.1.0" +preact@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.0.tgz#90e10264a221690484a56344437a353ffac08600" + integrity sha512-34iqY2qPWKAmsi+tNNwYCstta93P+zF1f4DLtsOUPh32uYImNzJY7h7EymCva+6RoJL01v3W3phSRD8jE0sFLg== + pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"