;
22 |
23 | export function createElement(
24 | type: FC
,
25 | props: (P & { children?: ComponentChildren }) | null,
26 | ...children: ComponentChildren[]
27 | ): VNode;
28 |
29 | export function createElement(type: any, props: any, ...children: any) {
30 | return {
31 | type,
32 | props: {
33 | children,
34 | ...props,
35 | },
36 | };
37 | }
38 | export namespace createElement {
39 | export import JSX = JSXInternal;
40 | }
41 |
42 | export function h(
43 | type: string,
44 | props: (HTMLAttributes & { className?: string }) | null,
45 | ...children: ComponentChildren[]
46 | ): VNode;
47 |
48 | export function h(
49 | type: FC
,
50 | props: (P & { className?: string; children?: ComponentChildren }) | null,
51 | ...children: Children[]
52 | ): VNode;
53 |
54 | export function h(type: any, props: any, ...children: any) {
55 | // FXIME: should not use `.flat()`
56 | return createElement(type, props, ...children.flat());
57 | }
58 |
59 | export const Fragment: FC = ({ children }) => {
60 | if (children) {
61 | return children as h.JSX.Element
62 | }
63 | return null
64 | }
65 |
66 | export namespace h {
67 | export import JSX = JSXInternal;
68 | }
69 |
--------------------------------------------------------------------------------
/packages/lemon/src/renderToString.ts:
--------------------------------------------------------------------------------
1 | import { VNode } from ".";
2 |
3 | const arrayToString = (node: VNode<{ children: any[] }>): string => {
4 | return node.props.children.map(renderToString).join("");
5 | };
6 |
7 | const attrToString = ([key, value]: [string, unknown]): string => {
8 | switch (key) {
9 | case "async": case "defer":
10 | return !!value ? key : '';
11 | case "className":
12 | return `class="${value}"`;
13 | case "aria-relevant":
14 | return `aria-relevant="${(value as Array).join(" ")}"`;
15 | default:
16 | return `${key}="${value}"`;
17 | }
18 | };
19 |
20 | export function renderToString(node: Array> | VNode | string | null): string {
21 | if (!node) return "";
22 | if (typeof node === "string") return node;
23 | if (Array.isArray(node)) {
24 | return node.map(renderToString).join("")
25 | }
26 | const childrenAsArray =
27 | typeof node.props === "object" &&
28 | node.props != null &&
29 | node.props.children &&
30 | node.props.children.length >= 1;
31 | if (typeof node.type === "string") {
32 | const attr = Object.entries(node.props)
33 | .filter(([k, _]) => k !== "children")
34 | .map(attrToString)
35 | .join(" ")
36 | .trim();
37 | const typeAndAttr = `${node.type} ${attr}`.trim();
38 | if (childrenAsArray) {
39 | return `<${typeAndAttr}>${arrayToString(node)}${node.type}>`;
40 | }
41 | return `<${typeAndAttr}>${node.type}>`;
42 | }
43 | if (childrenAsArray) {
44 | const { children, ...props } = node.props;
45 | return renderToString(
46 | node.type({
47 | ...props,
48 | children: children.map((c: any) => {
49 | return renderToString(c);
50 | }),
51 | })
52 | );
53 | }
54 | return renderToString(node.type(node.props));
55 | }
56 |
--------------------------------------------------------------------------------
/packages/melon/for-local/nginx.templ.conf:
--------------------------------------------------------------------------------
1 | worker_processes auto;
2 |
3 | error_log $GHQ_ROOT/github.com/maxmellon/kajitsu/packages/melon/log/error.log;
4 |
5 | events {
6 | worker_connections 1024;
7 | }
8 |
9 | http {
10 | default_type application/octet-stream;
11 | server_tokens off;
12 |
13 | log_format main 'time:$time_iso8601\t'
14 | 'protocol:$server_protocol\t'
15 | 'method:$request_method\t'
16 | 'path:$request_uri\t'
17 | 'status:$status\t'
18 | 'request_time:$request_time\t'
19 | 'upstream_response_time:$upstream_response_time\t'
20 | 'body_bytes_sent:$body_bytes_sent\t'
21 | 'remote_addr:$remote_addr\t'
22 | 'x_forwarded_for:$http_x_forwarded_for\t'
23 | 'referer:$http_referer\t'
24 | 'user_agent:$http_user_agent\t';
25 |
26 | access_log $GHQ_ROOT/github.com/maxmellon/kajitsu/packages/melon/log/access.log main;
27 |
28 | upstream app {
29 | server 127.0.0.1:3000;
30 | keepalive 32;
31 | }
32 |
33 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
34 |
35 | server {
36 | listen 443 ssl http2;
37 | ssl_certificate $GHQ_ROOT/github.com/maxmellon/kajitsu/packages/melon/localhost.pem;
38 | ssl_certificate_key $GHQ_ROOT/github.com/maxmellon/kajitsu/packages/melon/localhost-key.pem;
39 |
40 | proxy_connect_timeout 5;
41 | proxy_send_timeout 30;
42 | proxy_read_timeout 30;
43 | proxy_intercept_errors on;
44 | proxy_hide_header X-Powered-By;
45 |
46 | location / {
47 | add_header Feature-Policy "sync-xhr 'none'";
48 | add_header X-Frame-Options "SAMEORIGIN";
49 | add_header X-XSS-Protection "1; mode=block";
50 | add_header X-Content-Type-Options "nosniff";
51 | add_header Strict-Transport-Security "max-age=31536000";
52 |
53 | proxy_pass http://app;
54 | }
55 | }
56 |
57 | server {
58 | listen 80;
59 | return 301 https://$host$request_uri;
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/packages/melon/src/assets/css/reset.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p,
2 | blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em,
3 | img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u,
4 | i, center, hr, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table,
5 | caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details,
6 | embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby,
7 | section, summary, time, mark, audio, video {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | font: inherit;
12 | vertical-align: baseline;
13 | }
14 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
15 | display: block;
16 | }
17 | ol, ul {
18 | list-style: none;
19 | }
20 | blockquote, q {
21 | quotes: none;
22 | }
23 | blockquote::before, blockquote::after, q::before, q::after {
24 | content: '';
25 | }
26 | table {
27 | border-collapse: collapse;
28 | border-spacing: 0;
29 | }
30 | img {
31 | max-width: 100%;
32 | border-style: none;
33 | }
34 | :disabled {
35 | cursor: not-allowed;
36 | }
37 | * {
38 | box-sizing: border-box;
39 | }
40 | *::before, *::after {
41 | box-sizing: inherit;
42 | }
43 | body {
44 | background: transparent;
45 | font-family: "Helvetica Neue",
46 | Arial,
47 | "Hiragino Kaku Gothic ProN",
48 | "Hiragino Sans",
49 | Meiryo,
50 | sans-serif;
51 | font-size: 16px;
52 | line-height: 1.75;
53 | }
54 | h1, h2, h3, h4, h5, h6 {
55 | font-weight: 700;
56 | }
57 | button {
58 | border: none;
59 | padding: 0;
60 | background: none;
61 | color: inherit;
62 | font: inherit;
63 | appearance: none;
64 | box-shadow: 0 2px 8px 0 rgba(24, 74, 70, 0.2);
65 | text-shadow: 0 2px 8px 0 rgba(24, 74, 70, 0.2);
66 | }
67 |
68 | button:active {
69 | box-shadow: none;
70 | text-shadow: none;
71 | transform: translateY(1px);
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/packages/zakuro/src/explorer/assets/index.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p,
2 | blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em,
3 | img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u,
4 | i, center, hr, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table,
5 | caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details,
6 | embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby,
7 | section, summary, time, mark, audio, video {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | font: inherit;
12 | vertical-align: baseline;
13 | }
14 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
15 | display: block;
16 | }
17 | ol, ul {
18 | list-style: none;
19 | }
20 | blockquote, q {
21 | quotes: none;
22 | }
23 | blockquote::before, blockquote::after, q::before, q::after {
24 | content: '';
25 | }
26 | table {
27 | border-collapse: collapse;
28 | border-spacing: 0;
29 | }
30 | img {
31 | max-width: 100%;
32 | border-style: none;
33 | }
34 | :disabled {
35 | cursor: not-allowed;
36 | }
37 | * {
38 | box-sizing: border-box;
39 | }
40 | *::before, *::after {
41 | box-sizing: inherit;
42 | }
43 | body {
44 | background: transparent;
45 | font-family: "Helvetica Neue",
46 | Arial,
47 | "Hiragino Kaku Gothic ProN",
48 | "Hiragino Sans",
49 | Meiryo,
50 | sans-serif;
51 | font-size: 16px;
52 | line-height: 1.75;
53 | }
54 | h1, h2, h3, h4, h5, h6 {
55 | font-weight: 700;
56 | }
57 | button {
58 | border: none;
59 | padding: 0;
60 | background: none;
61 | color: inherit;
62 | font: inherit;
63 | appearance: none;
64 | box-shadow: 0 2px 8px 0 rgba(24, 74, 70, 0.2);
65 | text-shadow: 0 2px 8px 0 rgba(24, 74, 70, 0.2);
66 | }
67 |
68 | button:active {
69 | box-shadow: none;
70 | text-shadow: none;
71 | transform: translateY(1px);
72 | }
73 |
--------------------------------------------------------------------------------
/packages/lemon/src/role.ts:
--------------------------------------------------------------------------------
1 | export type Role =
2 | | "alert"
3 | | "alertdialog"
4 | | "application"
5 | | "article"
6 | | "banner"
7 | | "button"
8 | | "button"
9 | | "cell"
10 | | "cell"
11 | | "checkbox"
12 | | "checkbox"
13 | | "columnheader"
14 | | "columnheader"
15 | | "combobox"
16 | | "command"
17 | | "complementary"
18 | | "composite"
19 | | "contentinfo"
20 | | "definition"
21 | | "dialog"
22 | | "directory"
23 | | "document"
24 | | "feed"
25 | | "figure"
26 | | "form"
27 | | "grid"
28 | | "gridcell"
29 | | "gridcell"
30 | | "group"
31 | | "heading"
32 | | "heading"
33 | | "img"
34 | | "input"
35 | | "landmark"
36 | | "link"
37 | | "link"
38 | | "list"
39 | | "listbox"
40 | | "listitem"
41 | | "log"
42 | | "main"
43 | | "marquee"
44 | | "math"
45 | | "menu"
46 | | "menubar"
47 | | "menuitem"
48 | | "menuitem"
49 | | "menuitemcheckbox"
50 | | "menuitemcheckbox"
51 | | "menuitemradio"
52 | | "menuitemradio"
53 | | "navigation"
54 | | "none"
55 | | "note"
56 | | "option"
57 | | "option"
58 | | "presentation"
59 | | "progressbar"
60 | | "radio"
61 | | "radio"
62 | | "radiogroup"
63 | | "range"
64 | | "region"
65 | | "roletype"
66 | | "row"
67 | | "row"
68 | | "rowgroup"
69 | | "rowgroup"
70 | | "rowheader"
71 | | "rowheader"
72 | | "scrollbar"
73 | | "search"
74 | | "searchbox"
75 | | "section"
76 | | "sectionhead"
77 | | "select"
78 | | "separator"
79 | | "separator"
80 | | "slider"
81 | | "spinbutton"
82 | | "status"
83 | | "structure"
84 | | "switch"
85 | | "switch"
86 | | "tab"
87 | | "tab"
88 | | "table"
89 | | "tablist"
90 | | "tabpanel"
91 | | "term"
92 | | "textbox"
93 | | "timer"
94 | | "toolbar"
95 | | "tooltip"
96 | | "tooltip"
97 | | "tree"
98 | | "tree"
99 | | "treegrid"
100 | | "treeitem"
101 | | "treeitem"
102 | | "widget"
103 | | "window";
104 |
--------------------------------------------------------------------------------
/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes auto;
3 |
4 | error_log /var/log/nginx/error.log warn;
5 | pid /var/run/nginx.pid;
6 |
7 | events {
8 | worker_connections 1024;
9 | }
10 |
11 | http {
12 | include /etc/nginx/mime.types;
13 | default_type application/octet-stream;
14 | server_tokens off;
15 |
16 | log_format main 'time:$time_iso8601\t'
17 | 'protocol:$server_protocol\t'
18 | 'method:$request_method\t'
19 | 'path:$request_uri\t'
20 | 'status:$status\t'
21 | 'request_time:$request_time\t'
22 | 'upstream_response_time:$upstream_response_time\t'
23 | 'body_bytes_sent:$body_bytes_sent\t'
24 | 'remote_addr:$remote_addr\t'
25 | 'x_forwarded_for:$http_x_forwarded_for\t'
26 | 'referer:$http_referer\t'
27 | 'user_agent:$http_user_agent\t';
28 |
29 | access_log /var/log/nginx/access.log main;
30 |
31 | proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app:50m inactive=1d use_temp_path=off;
32 |
33 | upstream app {
34 | server app:3000;
35 | keepalive 32;
36 | }
37 |
38 | server {
39 | listen 80;
40 | keepalive_timeout 5;
41 |
42 | proxy_connect_timeout 5;
43 | proxy_send_timeout 30;
44 | proxy_read_timeout 30;
45 | proxy_intercept_errors on;
46 | proxy_hide_header X-Powered-By;
47 |
48 | location / {
49 | proxy_cache app;
50 | proxy_cache_valid 120m;
51 |
52 | add_header Permissions-Policy "sync-xhr 'none'";
53 | add_header X-Frame-Options "SAMEORIGIN";
54 | add_header X-XSS-Protection "1; mode=block";
55 | add_header X-Content-Type-Options "nosniff";
56 | add_header Strict-Transport-Security "max-age=31536000";
57 |
58 | proxy_pass http://app;
59 | }
60 |
61 | }
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/packages/lemon/src/wai-aria.ts:
--------------------------------------------------------------------------------
1 | type TriState = "true" | "false" | "mixed" | "undefined"; // default "undefined"
2 |
3 | /**
4 | * refs: https://www.w3.org/TR/2014/REC-wai-aria-20140320/states_and_properties.html
5 | */
6 | export type WAIAria = Partial<{
7 | "aria-activedescendant": "";
8 | "aria-atomic": "true" | "false"; // defalut "false"
9 | "aria-autocomplete": "inline" | "list" | "both" | "none"; // default "none"
10 | "aria-busy": "true" | "false"; // default "false"
11 | "aria-checked": TriState;
12 | "aria-controls": string;
13 | "aria-describedby": string;
14 | "aria-disabled": "true" | "false"; // default "false"
15 | "aria-dropeffect": "copy" | "move" | "link" | "execute" | "popup" | "none"; // default "none"
16 | "aria-expanded": "true" | "false" | "undefined"; // default "undefined"
17 | "aria-flowto": string;
18 | "aria-grabbed": "true" | "false" | "undefined"; // default "undefined"
19 | "aria-haspopup": "true" | "false"; // default "false"
20 | "aria-hidden": "true" | "false"; // default "false"
21 | "aria-invalid": "grammar" | "false" | "spelling" | "true"; // default "false"
22 | "aria-label": string;
23 | "aria-labelledby": string;
24 | "aria-level": number;
25 | "aria-live": "log" | "status" | "alert" | "progressbar" | "marquee" | "timer";
26 | "aria-multiline": "true" | "false"; // default "false"
27 | "aria-multiselectable": "true" | "false"; // default "false"
28 | "aria-orientation": "vertical" | "horizontal"; // default "horizontal"
29 | "aria-owns": string;
30 | "aria-posinset": number;
31 | "aria-pressed": TriState;
32 | "aria-readonly": "true" | "false"; // default "false"
33 | "aria-relevant": Array<"additions" | "removals" | "text" | "all">; // default "additions text"
34 | "aria-required": "true" | "false";
35 | "aria-selected": "true" | "false" | "undefined"; // default "undefined";
36 | "aria-setsize": number;
37 | "aria-sort": "ascending" | "descending" | "none" | "other"; // default "none"
38 | "aria-valuemax": number;
39 | "aria-valuemin": number;
40 | "aria-valuenow": number;
41 | "aria-valuetext": string;
42 | }>;
43 |
--------------------------------------------------------------------------------
/packages/knife/src/tokenizer.ts:
--------------------------------------------------------------------------------
1 | const tokens = new Map([
2 | ['[', 'L_SQUARE'],
3 | [']', 'R_SQUARE'],
4 | ['`', 'BACKQUOTE'],
5 | ['*', 'ASTAH'],
6 | ['(', 'L_BRACE'],
7 | [')', 'R_BRACE'],
8 | ['#', 'SHARP'],
9 | [';', 'SEMICORON']
10 | ])
11 |
12 | const EOS = '#-- EOS --#'
13 |
14 | const whitespaces = new Set([
15 | ' ',
16 | '\n',
17 | '\r',
18 | '\t'
19 | ])
20 |
21 | export type Token = {
22 | type: string
23 | literal: string
24 | }
25 |
26 | export class Tokenizer {
27 | private rawText: string
28 | // walk by code point
29 | private readPosition: number = 0;
30 | // current code point
31 | private char: string
32 | private isEnd: boolean = false
33 |
34 | constructor(input: string) {
35 | this.rawText = input
36 | this.char = String.fromCodePoint(0)
37 | }
38 |
39 | *tokenize(): Generator {
40 | while (!this.isEnd) {
41 | console.log(this.readPosition)
42 | this.read()
43 | this.skipWhiteSpace()
44 | const maybeToken = tokens.get(this.char)
45 | let token: Token
46 | if (this.char === EOS) {
47 | token = {
48 | type: 'EOS',
49 | literal: 'EOS',
50 | }
51 | } else if (typeof maybeToken === 'undefined') {
52 | this.readPlainText()
53 | token = {
54 | type: 'PLAIN',
55 | literal: this.char.toString()
56 | }
57 | } else {
58 | token = {
59 | type: maybeToken,
60 | literal: this.char.toString()
61 | }
62 | }
63 | yield token
64 | }
65 | return true
66 | }
67 |
68 | skipWhiteSpace() {
69 | while (whitespaces.has) {
70 | this.read()
71 | }
72 | }
73 |
74 | readPlainText() {
75 | while (!tokens.has(this.char) || !whitespaces.has(this.char)) {
76 | const codePoint = this.rawText.codePointAt(this.readPosition)
77 | if (!codePoint) return
78 | this.char += String.fromCodePoint(codePoint)
79 | this.readPosition++
80 | }
81 | }
82 |
83 | read() {
84 | const codePoint = this.rawText.codePointAt(this.readPosition)
85 | // is end of text if code point is undefined.
86 | if (typeof codePoint === 'undefined') {
87 | this.char = EOS
88 | this.isEnd = true
89 | } else {
90 | this.char = String.fromCodePoint(codePoint)
91 | }
92 | this.readPosition++
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/packages/melon/src/pages/blog/articles.tsx:
--------------------------------------------------------------------------------
1 | import { h, Fragment } from "@kajitsu/lemon";
2 | import { PageTemplate } from "../../components/templates/page";
3 | import { Nav } from "../../components/organisms/nav";
4 |
5 | type Articles = {
6 | [year: string]: Array<{
7 | title: string;
8 | date: string;
9 | key: string;
10 | extendsHeader: () => Promise;
11 | renderer: (nonce?: string) => Promise;
12 | }>;
13 | };
14 |
15 | export const articles: Articles = {
16 | "2020": [
17 | {
18 | title: "node modules に依存しない blog を作っている話",
19 | date: '2020-12-21',
20 | key: "20201221",
21 | extendsHeader: async () => {
22 | return (
23 | <>
24 |
25 |
26 |
27 |
28 | >
29 | );
30 | },
31 | renderer: async () => {
32 | const header = ;
33 | const Article = await import("./20201221").then((r) => r.Article);
34 | return (
35 |
36 |
37 |
38 | );
39 | },
40 | },
41 | ],
42 | "2021": [
43 | {
44 | title: "tc39/proposal-pipeline-operator の過去と現状",
45 | date: '2021-12-04',
46 | key: "20211204",
47 | extendsHeader: async () => {
48 | return (
49 | <>
50 |
51 |
52 |
53 |
54 |
55 | >
56 | );
57 | },
58 | renderer: async (nonce?: string) => {
59 | const header = ;
60 | const Article = await import("./20211204").then((r) => r.Article);
61 | return (
62 |
63 |
64 |
65 | );
66 | },
67 | },
68 | ],
69 | };
70 |
--------------------------------------------------------------------------------
/packages/suica/src/index.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage, ServerResponse } from "http";
2 | import url from "url";
3 |
4 | export * as middleware from "./middleware";
5 |
6 | declare global {
7 | namespace Suica {
8 | export interface Context {}
9 | }
10 | }
11 |
12 | export interface RequestHandler {
13 | (
14 | ctx: Suica.Context,
15 | req: IncomingMessage,
16 | res: ServerResponse,
17 | next: (err?: Error) => Promise
18 | ): Promise;
19 | }
20 |
21 | interface SuicaMiddlwareRegisterWithPath {
22 | (path: string | RegExp, handler: RequestHandler): void;
23 | }
24 |
25 | interface SuicaMiddlewareRegisterAll {
26 | (handler: RequestHandler): void;
27 | }
28 |
29 | interface SuicaMiddlewareRegister {
30 | (path: string | RegExp, handler: RequestHandler): void;
31 | (handler: RequestHandler): void;
32 | }
33 |
34 | class App {
35 | readonly use: SuicaMiddlewareRegister;
36 | readonly get: SuicaMiddlwareRegisterWithPath;
37 | private routing: Array<[string | RegExp, RequestHandler]> = [];
38 |
39 | constructor() {
40 | this.use = (...args: any[]) => {
41 | if (args.length >= 3) throw new Error("arguments error");
42 | if (args.length == 2) {
43 | this.routing.push([args[0], args[1]]);
44 | return;
45 | }
46 | this.routing.push(["", args[0]]);
47 | };
48 |
49 | this.get = (path: string | RegExp, middleware: RequestHandler) => {
50 | const enhancedMiddleware: RequestHandler = async (ctx, req, res, next) => {
51 | if (req.method !== "GET") {
52 | res.statusCode = 404;
53 | res.end()
54 | return
55 | }
56 | await middleware(ctx, req, res, next)
57 | }
58 | this.routing.push([path, enhancedMiddleware])
59 | }
60 | }
61 |
62 | async run(req: IncomingMessage, res: ServerResponse): Promise {
63 | let idx = 0;
64 | let ctx: Suica.Context = {};
65 | while (idx < this.routing.length) {
66 | const next = async () => void idx++;
67 | const [pathOrRegExp, handler] = this.routing[idx];
68 | const currentPath = url.parse(req.url || "/").pathname;
69 |
70 | const isMatch =
71 | typeof pathOrRegExp === "string"
72 | ? currentPath === pathOrRegExp
73 | : pathOrRegExp.test(currentPath ?? "");
74 |
75 | if (isMatch) {
76 | const prevIdx = idx;
77 | await handler(ctx, req, res, next);
78 | if (prevIdx === idx) break;
79 | } else {
80 | idx++;
81 | }
82 | }
83 | if (!res.writableEnded) {
84 | res.statusCode = 404;
85 | res.write("");
86 | res.end();
87 | }
88 | }
89 | }
90 |
91 | export function createSuica(): App {
92 | return new App();
93 | }
94 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '15 8 * * 2'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/packages/lemon/src/css.ts:
--------------------------------------------------------------------------------
1 | import { pipe } from "@kajitsu/ichigo";
2 | import { FC, h } from ".";
3 | import { InternalIntrinsicElements } from "./jsx";
4 |
5 | const randomString = (): string => {
6 | const str =
7 | 'le-' +
8 | Math.random().toString(36).substring(2, 5) +
9 | Math.random().toString(36).substring(2, 5);
10 | for (const ctx of allContext) {
11 | if (ctx.map.has(str)) return randomString();
12 | }
13 | return str;
14 | };
15 |
16 | export interface Context {
17 | map: Map,
18 | getClassNames(): string[],
19 | set(): this
20 | remove(): void
21 | }
22 |
23 | let currentContext: Context | undefined;
24 | const allContext: Context[] = [];
25 |
26 |
27 | export const createStyleContext = () => {
28 | const ctx: Context = {
29 | map: new Map(),
30 | getClassNames(): string[] {
31 | return Array.from(this.map.keys());
32 | },
33 | set() {
34 | currentContext = this;
35 | allContext.push(ctx);
36 | return this;
37 | },
38 | remove() {
39 | const idx = allContext.findIndex((ctx) => ctx === this);
40 | if (idx === -1) return;
41 | allContext.splice(idx, 1);
42 | },
43 | };
44 | return ctx;
45 | };
46 |
47 | export const refreshContext = () => (currentContext = void 0);
48 |
49 | const trimSpace = (str: string): string => str.replace(/\s+/g, " ");
50 | const trimNewLine = (str: string): string => str.replace("\n", "");
51 | const trim = (str: string) => pipe(str, trimSpace, trimNewLine);
52 |
53 | /**
54 | * @summary should be call renderToString before this.
55 | */
56 | export const renderToStyleString = (ctx: Context) => (
57 | Array
58 | .from(ctx.map.entries())
59 | .map(
60 | ([key, val]) => `.${key} {${trim(val)
61 | .split(";")
62 | .filter((x) => x)
63 | .join(";")}}`
64 | )
65 | .join("")
66 | )
67 |
68 | type PropTypes = T extends FC<
69 | infer P
70 | >
71 | ? P
72 | : T extends keyof InternalIntrinsicElements
73 | ? InternalIntrinsicElements[T]
74 | : unknown;
75 |
76 | export const css = (node: FC | keyof InternalIntrinsicElements) => (
77 | strings: TemplateStringsArray,
78 | ...args: string[]
79 | ): FC
> => {
80 | const className = randomString();
81 | const style = strings.reduce(
82 | (acc, cur, idx) => (args[idx] ? acc + cur + args[idx] : acc + cur),
83 | ""
84 | );
85 | const Component: FC
= (props) => {
86 | if (!currentContext)
87 | throw new Error("you should create context, before render");
88 | currentContext.map.set(className, style);
89 | // @ts-expect-error [2769] No overload matches this call.
90 | return h(node, { className, ...props });
91 | };
92 | return Component;
93 | };
94 |
--------------------------------------------------------------------------------
/packages/zakuro/src/explorer/index.tsx:
--------------------------------------------------------------------------------
1 | import { createServer, IncomingMessage, ServerResponse } from "http";
2 | import { on } from "events";
3 | import { readFile } from "fs";
4 | import { resolve } from "path";
5 | import { promisify } from "util";
6 | import { createSuica } from "@kajitsu/suica";
7 | import {
8 | FC,
9 | h,
10 | renderToString,
11 | renderToStyleString,
12 | createStyleContext,
13 | } from "@kajitsu/lemon";
14 | import { atomsStories } from "./import";
15 | import { Root } from "./root";
16 |
17 | type RequestEventIterator = AsyncIterableIterator<
18 | [IncomingMessage, ServerResponse]
19 | >;
20 |
21 | const readFileAsync = promisify(readFile);
22 |
23 | const suica = createSuica();
24 |
25 | const Html: FC<{ css?: string }> = ({ children, css }) => (
26 |
27 |
28 |
29 |
30 |
31 | {typeof css === "string" && }
32 |
33 | {children}
34 |
35 |
36 | );
37 |
38 | atomsStories.map((scenario) => {
39 | suica.use(`/${scenario.key}`, async (_ctx, _req, res) => {
40 | const styleContext = createStyleContext().set();
41 | const children = renderToString(scenario.story());
42 | const css = renderToStyleString(styleContext);
43 | res.write(renderToString({children}));
44 | res.end();
45 | });
46 | });
47 |
48 | suica.use("/assets/index.js", async (_ctx, req, res, next) => {
49 | if (req.method !== "GET") return await next();
50 | const js = await readFileAsync(resolve(__dirname, "assets", "index.js"));
51 | res.setHeader("Content-Type", "text/javascript");
52 | res.write(js);
53 | res.end();
54 | });
55 |
56 | suica.use("/assets/index.css", async (_ctx, req, res, next) => {
57 | if (req.method !== "GET") return await next();
58 | const css = await readFileAsync(resolve(__dirname, "assets", "index.css"));
59 | res.setHeader("Content-Type", "text/css");
60 | res.write(css);
61 | res.end();
62 | });
63 |
64 | const styleContext = createStyleContext().set();
65 | const root = renderToString();
66 | const style = renderToStyleString(styleContext);
67 |
68 | suica.use("/assets/explorer.css", async (_ctx, req, res, next) => {
69 | if (req.method !== "GET") return await next();
70 | res.setHeader("Content-Type", "text/css");
71 | res.write(style);
72 | res.end();
73 | });
74 |
75 | suica.use("/", async (_ctx, req, res, next) => {
76 | if (req.method !== "GET") return await next();
77 | res.write(renderToString({root}));
78 | res.end();
79 | });
80 |
81 | // TODO: https://github.com/microsoft/TypeScript/pull/37424
82 | !(async () => {
83 | const itr: RequestEventIterator = on(createServer().listen(6789), "request");
84 | for await (const [req, res] of itr) //
85 | await suica.run(req, res);
86 | })();
87 |
--------------------------------------------------------------------------------
/packages/melon/src/assets/css/markdown.css:
--------------------------------------------------------------------------------
1 | #mr {
2 | padding-bottom: 50px;
3 | }
4 |
5 | #mr h1 {
6 | font-size: 32px;
7 | margin: 20px 0;
8 | }
9 |
10 | #mr h1::before {
11 | font-size: 28px;
12 | content: '#';
13 | margin-right: 20px;
14 | }
15 |
16 | #mr blockquote {
17 | padding: 10px;
18 | }
19 |
20 | #mr blockquote > p::before {
21 | display: inline;
22 | width: 4px;
23 | height: 2em;
24 | background-color: #eee;
25 | content: ' ';
26 | position: absolute;
27 | margin-left: -20px;
28 | }
29 |
30 | #mr blockquote > p {
31 | padding-left: 20px;
32 | }
33 |
34 | @media (prefers-color-scheme: dark) {
35 | #mr blockquote > p::before {
36 | background-color: #000;
37 | }
38 | }
39 |
40 | #mr h2 {
41 | font-size: 28px;
42 | margin: 16px 0;
43 | }
44 |
45 | #mr h2::before {
46 | font-size: 24px;
47 | content: '##';
48 | margin-right: 16px;
49 | }
50 |
51 | #mr h3 {
52 | font-size: 20px;
53 | margin: 12px 0;
54 | }
55 |
56 | #mr h4 {
57 | font-size: 18px;
58 | margin: 12px 0;
59 | }
60 |
61 | #mr h4::before {
62 | font-size: 14px;
63 | content: '**';
64 | margin: 8px 0;
65 | }
66 |
67 | #mr h4::after {
68 | font-size: 14px;
69 | content: '**';
70 | margin: 8px 0;
71 | }
72 |
73 | #mr strong {
74 | margin: 12px 0;
75 | font-weight: 800;
76 | }
77 |
78 | #mr strong::before {
79 | content: '**';
80 | margin: 3px 0;
81 | }
82 |
83 | #mr strong::after {
84 | content: '**';
85 | margin: 3px 0;
86 | }
87 |
88 | #mr em {
89 | font-weight: 700;
90 | }
91 |
92 | #mr h3::before {
93 | font-size: 16px;
94 | content: '###';
95 | margin-right: 8px;
96 | }
97 |
98 | #mr pre::before {
99 | display: block;
100 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
101 | content: '```';
102 | height: 10px;
103 | margin-top: 20px;
104 | }
105 |
106 | #mr pre::after {
107 | display: block;
108 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
109 | content: '```';
110 | margin-bottom: 20px;
111 | }
112 |
113 | #mr code {
114 | display: inline;
115 | background-color: #eee;
116 | padding: 2px 8px;
117 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
118 | white-space: nowrap;
119 | }
120 |
121 | @media all and (max-width: 800px) {
122 | #mr code {
123 | white-space: pre-wrap;
124 | }
125 | }
126 |
127 | #mr code::before {
128 | display: inline;
129 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
130 | content: '`';
131 | }
132 |
133 | #mr code::after {
134 | display: inline;
135 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
136 | content: '`';
137 | }
138 |
139 | #mr pre > code::after {
140 | content: '';
141 | }
142 |
143 | #mr pre > code::before {
144 | content: '';
145 | }
146 |
147 | @media (prefers-color-scheme: dark) {
148 | #mr code {
149 | background-color: #000;
150 | }
151 | }
152 |
153 | #mr pre > code {
154 | display: block;
155 | background-color: #eee;
156 | border-left: 4px solid #353;
157 | padding: 20px;
158 | margin: 10px 5px;
159 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
160 | overflow: scroll;
161 | white-space: pre;
162 | }
163 |
164 | @media (prefers-color-scheme: dark) {
165 | #mr pre > code {
166 | background-color: #000;
167 | }
168 | }
169 |
170 | #mr ul {
171 | margin: 10px 0;
172 | list-style-type: none;
173 | margin-left: 16px;
174 | }
175 |
176 | #mr ul > li::before {
177 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
178 | font-size: 16px;
179 | margin-right: 16px;
180 | content: '*';
181 | }
182 |
183 | #mr ul > li > ul {
184 | margin: 10px 0;
185 | list-style-type: none;
186 | margin-left: 26px;
187 | }
188 |
189 | #mr ul > li > ul > li::before {
190 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
191 | font-size: 16px;
192 | margin-right: 16px;
193 | content: '*';
194 | }
195 |
--------------------------------------------------------------------------------
/packages/lemon/src/rfc2978.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @see https://www.iana.org/assignments/character-sets/character-sets.xhtml
3 | */
4 | export type RFC2978 =
5 | | "US-ASCII"
6 | | "ISO_8859-1:1987"
7 | | "ISO_8859-2:1987"
8 | | "ISO_8859-3:1988"
9 | | "ISO_8859-4:1988"
10 | | "ISO_8859-5:1988"
11 | | "ISO_8859-6:1987"
12 | | "ISO_8859-7:1987"
13 | | "ISO_8859-8:1988"
14 | | "ISO_8859-9:1989"
15 | | "ISO-8859-10"
16 | | "ISO_6937-2-add"
17 | | "JIS_X0201"
18 | | "JIS_Encoding"
19 | | "Shift_JIS"
20 | | "Extended_UNIX_Code_Packed_Format_for_Japanese"
21 | | "Extended_UNIX_Code_Fixed_Width_for_Japanese"
22 | | "BS_4730"
23 | | "SEN_850200_C"
24 | | "IT"
25 | | "ES"
26 | | "DIN_66003"
27 | | "NS_4551-1"
28 | | "NF_Z_62-010"
29 | | "ISO-10646-UTF-1"
30 | | "ISO_646.basic:1983"
31 | | "INVARIANT"
32 | | "ISO_646.irv:1983"
33 | | "NATS-SEFI"
34 | | "NATS-SEFI-ADD"
35 | | "NATS-DANO"
36 | | "NATS-DANO-ADD"
37 | | "SEN_850200_B"
38 | | "KS_C_5601-1987"
39 | | "ISO-2022-KR"
40 | | "EUC-KR"
41 | | "ISO-2022-JP"
42 | | "ISO-2022-JP-2"
43 | | "JIS_C6220-1969-jp"
44 | | "JIS_C6220-1969-ro"
45 | | "PT"
46 | | "greek7-old"
47 | | "latin-greek"
48 | | "NF_Z_62-010_(1973)"
49 | | "Latin-greek-1"
50 | | "ISO_5427"
51 | | "JIS_C6226-1978"
52 | | "BS_viewdata"
53 | | "INIS"
54 | | "INIS-8"
55 | | "INIS-cyrillic"
56 | | "ISO_5427:1981"
57 | | "ISO_5428:1980"
58 | | "GB_1988-80"
59 | | "GB_2312-80"
60 | | "NS_4551-2"
61 | | "videotex-suppl"
62 | | "PT2"
63 | | "ES2"
64 | | "MSZ_7795.3"
65 | | "JIS_C6226-1983"
66 | | "greek7"
67 | | "ASMO_449"
68 | | "iso-ir-90"
69 | | "JIS_C6229-1984-a"
70 | | "JIS_C6229-1984-b"
71 | | "JIS_C6229-1984-b-add"
72 | | "JIS_C6229-1984-hand"
73 | | "JIS_C6229-1984-hand-add"
74 | | "JIS_C6229-1984-kana"
75 | | "ISO_2033-1983"
76 | | "ANSI_X3.110-1983"
77 | | "T.61-7bit"
78 | | "T.61-8bit"
79 | | "ECMA-cyrillic"
80 | | "CSA_Z243.4-1985-1"
81 | | "CSA_Z243.4-1985-2"
82 | | "CSA_Z243.4-1985-gr"
83 | | "ISO_8859-6-E"
84 | | "ISO_8859-6-I"
85 | | "T.101-G2"
86 | | "ISO_8859-8-E"
87 | | "ISO_8859-8-I"
88 | | "CSN_369103"
89 | | "JUS_I.B1.002"
90 | | "IEC_P27-1"
91 | | "JUS_I.B1.003-serb"
92 | | "JUS_I.B1.003-mac"
93 | | "greek-ccitt"
94 | | "NC_NC00-10:81"
95 | | "ISO_6937-2-25"
96 | | "GOST_19768-74"
97 | | "ISO_8859-supp"
98 | | "ISO_10367-box"
99 | | "latin-lap"
100 | | "JIS_X0212-1990"
101 | | "DS_2089"
102 | | "us-dk"
103 | | "dk-us"
104 | | "KSC5636"
105 | | "UNICODE-1-1-UTF-7"
106 | | "ISO-2022-CN"
107 | | "ISO-2022-CN-EXT"
108 | | "UTF-8"
109 | | "ISO-8859-13"
110 | | "ISO-8859-14"
111 | | "ISO-8859-15"
112 | | "ISO-8859-16"
113 | | "GBK"
114 | | "GB18030"
115 | | "OSD_EBCDIC_DF04_15"
116 | | "OSD_EBCDIC_DF03_IRV"
117 | | "OSD_EBCDIC_DF04_1"
118 | | "ISO-11548-1"
119 | | "KZ-1048"
120 | | "ISO-10646-UCS-2"
121 | | "ISO-10646-UCS-4"
122 | | "ISO-10646-UCS-Basic"
123 | | "ISO-10646-Unicode-Latin1"
124 | | "ISO-10646-J-1"
125 | | "ISO-Unicode-IBM-1261"
126 | | "ISO-Unicode-IBM-1268"
127 | | "ISO-Unicode-IBM-1276"
128 | | "ISO-Unicode-IBM-1264"
129 | | "ISO-Unicode-IBM-1265"
130 | | "UNICODE-1-1"
131 | | "SCSU"
132 | | "UTF-7"
133 | | "UTF-16BE"
134 | | "UTF-16LE"
135 | | "UTF-16"
136 | | "CESU-8"
137 | | "UTF-32"
138 | | "UTF-32BE"
139 | | "UTF-32LE"
140 | | "BOCU-1"
141 | | "ISO-8859-1-Windows-3.0-Latin-1"
142 | | "ISO-8859-1-Windows-3.1-Latin-1"
143 | | "ISO-8859-2-Windows-Latin-2"
144 | | "ISO-8859-9-Windows-Latin-5"
145 | | "hp-roman8"
146 | | "Adobe-Standard-Encoding"
147 | | "Ventura-US"
148 | | "Ventura-International"
149 | | "DEC-MCS"
150 | | "IBM850"
151 | | "PC8-Danish-Norwegian"
152 | | "IBM862"
153 | | "PC8-Turkish"
154 | | "IBM-Symbols"
155 | | "IBM-Thai"
156 | | "HP-Legal"
157 | | "HP-Pi-font"
158 | | "HP-Math8"
159 | | "Adobe-Symbol-Encoding"
160 | | "HP-DeskTop"
161 | | "Ventura-Math"
162 | | "Microsoft-Publishing"
163 | | "Windows-31J"
164 | | "GB2312"
165 | | "Big5"
166 | | "macintosh"
167 | | "IBM037"
168 | | "IBM038"
169 | | "IBM273"
170 | | "IBM274"
171 | | "IBM275"
172 | | "IBM277"
173 | | "IBM278"
174 | | "IBM280"
175 | | "IBM281"
176 | | "IBM284"
177 | | "IBM285"
178 | | "IBM290"
179 | | "IBM297"
180 | | "IBM420"
181 | | "IBM423"
182 | | "IBM424"
183 | | "IBM437"
184 | | "IBM500"
185 | | "IBM851"
186 | | "IBM852"
187 | | "IBM855"
188 | | "IBM857"
189 | | "IBM860"
190 | | "IBM861"
191 | | "IBM863"
192 | | "IBM864"
193 | | "IBM865"
194 | | "IBM868"
195 | | "IBM869"
196 | | "IBM870"
197 | | "IBM871"
198 | | "IBM880"
199 | | "IBM891"
200 | | "IBM903"
201 | | "IBM904"
202 | | "IBM905"
203 | | "IBM918"
204 | | "IBM1026"
205 | | "EBCDIC-AT-DE"
206 | | "EBCDIC-AT-DE-A"
207 | | "EBCDIC-CA-FR"
208 | | "EBCDIC-DK-NO"
209 | | "EBCDIC-DK-NO-A"
210 | | "EBCDIC-FI-SE"
211 | | "EBCDIC-FI-SE-A"
212 | | "EBCDIC-FR"
213 | | "EBCDIC-IT"
214 | | "EBCDIC-PT"
215 | | "EBCDIC-ES"
216 | | "EBCDIC-ES-A"
217 | | "EBCDIC-ES-S"
218 | | "EBCDIC-UK"
219 | | "EBCDIC-US"
220 | | "UNKNOWN-8BIT"
221 | | "MNEMONIC"
222 | | "MNEM"
223 | | "VISCII"
224 | | "VIQR"
225 | | "KOI8-R"
226 | | "HZ-GB-2312"
227 | | "IBM866"
228 | | "IBM775"
229 | | "KOI8-U"
230 | | "IBM00858"
231 | | "IBM00924"
232 | | "IBM01140"
233 | | "IBM01141"
234 | | "IBM01142"
235 | | "IBM01143"
236 | | "IBM01144"
237 | | "IBM01145"
238 | | "IBM01146"
239 | | "IBM01147"
240 | | "IBM01148"
241 | | "IBM01149"
242 | | "Big5-HKSCS"
243 | | "IBM1047"
244 | | "PTCP154"
245 | | "Amiga-1251"
246 | | "KOI7-switched"
247 | | "BRF"
248 | | "TSCII"
249 | | "CP51932"
250 | | "windows-874"
251 | | "windows-1250"
252 | | "windows-1251"
253 | | "windows-1252"
254 | | "windows-1253"
255 | | "windows-1254"
256 | | "windows-1255"
257 | | "windows-1256"
258 | | "windows-1257"
259 | | "windows-1258"
260 | | "TIS-620"
261 | | "CP50220";
262 |
263 |
--------------------------------------------------------------------------------
/packages/melon/src/pages/blog/20201221/index.tsx:
--------------------------------------------------------------------------------
1 | import { h, FC } from "@kajitsu/lemon";
2 |
3 | export const Article: FC = () => (
4 |
5 | node modules なし blog を作っている話
6 |
7 | 今年の9月頃から,no dependencies で blog を作っています.(devDependencies には,jest と TypeScript を入れています)
8 | 正直,フレームワークを使えば blog くらいならすぐ作れるだろうと思っていたのと,
9 | フレームワークの使い方を覚えることが自分の成長に大きくつながるとは感じることができず, やっていて楽しい +
10 | 学習する余地がありそうな no dependencies で blog を作ることにしました.
11 |
12 |
13 | リポジトリはこちらです maxmellon/kajitsu
14 |
15 | 機能要件を考える
16 | ざっくり,自分がほしいなと思った要件を整理すると,
17 |
18 | - markdown で記事を書きたい
19 | - blog だけじゃなくて cookie や Cache-Controll header などを検証できる sandbox 環境も併設したい
20 | - Client Side で JavaScript は極力動かさない
21 | - jsx で,ある程度型が効くように
22 | - ただシンプルに html を返すだけで良い,それ以外は一旦後回し
23 |
24 | みたいな感じになった.なので,上記の要件を満たすために以下のモジュールを自作することにした.
25 |
26 | - node web framework
27 | - jsx (client side の runtime コードなしでもいい)
28 | - CSS-in-JS (これは,必要かどうかはおいておいて作ってみたかった)
29 | - markdown parser
30 |
31 | どこまでできたか
32 | node web framework
33 |
34 | express に近いようなインターフェースで作ってみた 現状,ヘルパー的な method がとにかく少なく,毎度 Content-Type
35 | を自分で書く必要があったりとまだまだ改善の余地がたくさんある. 使い勝手は以下のような感じ
36 |
37 |
38 | {`// logger middleware
39 | suica.use(async (_ctx, req, _res, next) => ${"{"}
40 | console.log(req.method, req.url)
41 | await next()
42 | ${"}"})
43 |
44 | suica.use("/", async (_ctx, _req, res) => ${"{"}
45 | res.setHeader("Content-Type", "text/html")
46 | res.write(\`<!DOCTYPE html><html><head></head><body></body></html>\`)
47 | res.end();
48 | ${"}"});
49 |
50 | !(async () => ${"{"}
51 | for await (const [req, res] of on(createServer().listen(3000), "request"))
52 | suica.run(req, res);
53 | ${"}"})();
54 | `}
55 |
56 |
57 | post リクエストが来たときに,何もしないと body が Buffer 型で入っており,非常に扱いにくいという点. request header
58 | の content-type を見て,その content-type から Readble から chunk をまとめて適切な object に変換する必要があった.
59 | 何気なく普段使っている express の body-parser ってそういうものなのねという発見があった.
60 |
61 | jsx
62 |
63 | client side の レンダリングの実装は間に合わなかった(今後やる予定) jsx
64 | 自体を作るのは思っている以上に簡単だったが,どちらかというと html および
65 | その周辺の知識が乏しさに気がつくきっかけとなった
66 |
67 | React のような設計指針はなく,シンプルに jsx でテンプレートを書きたい程度にしか思っていない.
68 |
69 |
70 | whatwg html semantics
71 | の仕様をみながら,どの要素がなんの属性を持つことができるのかを手を動かしながら学べたのは非常に勉強になった.
72 | Global Attributes や,
73 | マイクロデータ,{" "}
74 | WAI-ARIA,各要素のセマンティクス,正しい html
75 | とはなにか,いかに普段我々の書く html が雑なものかを考えさせられるきっかけになった.
76 |
77 | CSS-in-JS
78 |
79 | これに至っては,なんとなく動くものは作れたが... コンポーネント軸で開発をすすめると セレクタ詳細度
80 | がごちゃついて辛いのはわかるけれどそれを解決するのが CSS-in-JS なのかは割と今でも疑問.
81 |
↓ こんな感じで一応普通に使える
82 |
83 |
84 | {`export const Header: FC = ({ children }) => (
85 | <Header>
86 | {headerContents}
87 | </Header>
88 | );
89 |
90 | const Header = styled('header')\`
91 | position: sticky;
92 | z-index: 1;
93 | top: 0;
94 | background-color: var(--header-color);
95 | height: 40px;
96 | color: white;
97 | \``}
98 |
99 | markdown-parser
100 |
101 | アドベントカレンダーリリースまでに間に合わなかった.
102 | 正規表現でやるような実装にはしたくなく,パーサーを名乗るのであればちゃんと字句解析,意味解析をしようとしている.
103 | いまは,簡単な字句解析までができている.
104 |
105 | また,jsx で markdown を扱うとき次のように無駄な処理が入ることに課題を感じていた
106 |
107 | {`markdown - [parse] - AST(?) -> [render] -> html -> [parse] -> VDOM - [render] -> html`}
108 |
109 |
110 | markdown の AST 定めて その AST を VDOM に変換できれば,無駄にhtml を parseする必要がなくなる. jsx, VDOM
111 | を自作しているからこそできることだと思う
112 |
113 |
114 | {`markdown - [parse] -> AST -> [process] -> VDOM -> [render] -> html`}
115 |
116 |
117 | VDOM を使っているのに,VDOM ではなく,html を パースしてよしなに加工したり,critcal path をあれこれしたりする
118 | 無駄なことをしているツールが世の中に多いので,もっと VDOM に親和性が高い linter であったり,optimizer
119 | であったりを作る必要があるなと感じている.それには,html を json で表記するような標準の format
120 | が必要なのかもしれない?
121 |
122 | これからやること
123 | エンハンス開発
124 |
125 | - 自作 jsx によって作られたコンポーネントカタログ (ちょっとだけ動くくらいまで作っている)
126 | - テストランナーの自作
127 | - VDOM linter
128 | - 新しいjsx定義への変更
129 | -
130 | brotli するための middleware (for 自作 node web fromework)
131 |
132 | - zlib に入ったのは本当にありがたい
133 | - もちろん gzip, deflate への fallback も実装する
134 |
135 |
136 | - markdown内の syntax highlighter 実装
137 |
138 | ログ基盤の構築
139 |
140 | インフラ周りのツールを自作するかどうかは悩んでいるが,もうリリースしてしまったので取り急ぎ fluentd から Cloud
141 | Logging にログを流し込めるようにしたい これは,年内にも終わらせる
142 |
143 | サーバ監視
144 | mackerel が個人だと無料で使えて良さそう
145 | CSP のレポート先エンドポイントの実装
146 | 個人的にどんな CSP違反 が起きているのか見てみたい
147 | 感想
148 |
149 | html を node
150 | で返すというフレームワークを使えば秒でおわりそうなものを,仕様を読み込んだりしながら三ヶ月くらいかけて作ってきました.
151 | 何より一番よかったのはすごく楽しかったという点.なかなか,業務ではこういった開発はできないのでとても新鮮でよかったところです.
152 | この開発を通して,世の中に出ている ライブラリがどのように作られていたりとか,html
153 | の仕様とか学習できたのじゃないかと思います.
154 | そして,いかに標準APIを使いこなせていないか,知らないかが明らかになりました.
155 | また,ライブラリを採択するときにも,内部実装をみて筋が良いかどうかを判断するというのがこの開発を通して増えてきたと思う.
156 | 例えば,正規表現ベースのマークダウン系のライブラリだったり,オブザーバブルパターンを実装するためだけに Proxy
157 | を使っているものだったりと,「本当にそれ必要?」ってより具体的に問うことが以前に比べると成長できた点じゃないかなとおもいます.
158 |
159 |
160 | この開発では,whatwg の仕様を読むことのきっかけにはなったが,肝心の JavaScript
161 | の仕様を読むきっかけにはならなかったので, 今後は,JavaScript
162 | の仕様にも目を通して手続き一つ一つにも疑問を問いかけながら開発を楽しんでいきたいなと思いました.
163 |
164 |
165 | そして,仕様を読むきっかけになるというのは初めてスタート地点に立てたことであり,このレベルで満足せずもっと仕様を読み込んで
166 | 真の意味で「正しいものを正しく作る」エンジニアを目指していきたい .
167 |
168 |
169 | );
170 |
--------------------------------------------------------------------------------
/packages/melon/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { createServer, IncomingMessage, ServerResponse } from "http";
2 | import { randomBytes } from "crypto";
3 | import { on } from "events";
4 | import { readFile, readdirSync } from "fs";
5 | import { promisify } from "util";
6 | import { join } from "path";
7 | import { createSuica } from "@kajitsu/suica";
8 | import { pipe } from "@kajitsu/ichigo";
9 | import { h, Fragment, renderToString, createStyleContext, renderToStyleString } from "@kajitsu/lemon";
10 | import { Html } from "./components/templates/html";
11 | import { articles } from "./pages/blog/articles";
12 |
13 | const readFileAsync = promisify(readFile);
14 |
15 | const readAssets = (name: string): Promise =>
16 | pipe(
17 | name,
18 | (n) => join(__dirname, "assets", `${n}`),
19 | (p) => readFileAsync(p),
20 | );
21 |
22 | const images = readdirSync(join(__dirname, "assets", "images"))
23 |
24 | type RequestEventIterator = AsyncIterableIterator<[IncomingMessage, ServerResponse]>;
25 |
26 | const csp = (nonce: string) => `default-src 'self'; img-src 'self' data:; style-src 'self' 'nonce-${nonce}'; script-src 'self' 'nonce-${nonce}';`
27 |
28 | const suica = createSuica();
29 |
30 | if (process.env.NODE_ENV !== "production") {
31 | suica.use(async (_ctx, req, _res, next) => {
32 | console.log(req.method, req.url);
33 | await next();
34 | });
35 | }
36 |
37 | suica.get("/robots.txt", async (_ctx, _req, res) => {
38 | const robotsTxt = await readAssets("robots.txt");
39 | res.setHeader("Content-Type", "text/plain");
40 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
41 | res.write(robotsTxt);
42 | res.end();
43 | });
44 |
45 | suica.get("/manifest.json", async (_ctx, _req, res) => {
46 | const robotsTxt = await readAssets("manifest.json");
47 | res.setHeader("Content-Type", "application/manifest+json");
48 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
49 | res.write(robotsTxt);
50 | res.end();
51 | });
52 |
53 | for (const i of [72, 96, 128, 144, 152, 192, 384, 512]) {
54 | suica.get(`/icon-${i}x${i}.png`, async (_ctx, _req, res) => {
55 | const png = await readAssets(`icons/icon-${i}x${i}.png`);
56 | res.setHeader("Content-Type", "image/png");
57 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
58 | res.write(png);
59 | res.end();
60 | });
61 | }
62 |
63 | const contentType = {
64 | png: "png",
65 | svg: "svg+xml",
66 | };
67 |
68 | for (const i of images) {
69 | suica.get(`/images/${i}`, async (_ctx, _req, res) => {
70 | const png = await readAssets(`images/${i}`);
71 | const ext = i.split('.')[1] as 'png' | 'svg'
72 | res.setHeader("Content-Type", `image/${contentType[ext]}`);
73 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
74 | res.write(png);
75 | res.end();
76 | })
77 | }
78 |
79 | suica.get("/favicon.ico", async (_ctx, _req, res) => {
80 | const ico = await readAssets("favicon.ico");
81 | res.setHeader("Content-Type", "image/x-icon");
82 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
83 | res.write(ico);
84 | res.end();
85 | });
86 |
87 | suica.get("/markdown.css", async (_ctx, _req, res) => {
88 | const css = await readAssets("css/markdown.css");
89 | res.setHeader("Content-Type", "text/css");
90 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
91 | res.write(css);
92 | res.end();
93 | });
94 |
95 | suica.get("/main.css", async (_ctx, _req, res) => {
96 | const css = await Promise.all([readAssets("css/reset.css"), readAssets("css/theme.css")]).then((v) =>
97 | Buffer.concat(v),
98 | );
99 | res.setHeader("Content-Type", "text/css");
100 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
101 | res.write(css);
102 | res.end();
103 | });
104 |
105 | suica.get("/sw.h6u4siOcagcPl8vd.js", async (_ctx, _req, res) => {
106 | const sw = await readAssets("sw.js");
107 | res.setHeader("Content-Type", "application/javascript");
108 | res.setHeader("Cache-Control", "public, immutable, max-age=2592000");
109 | res.write(sw);
110 | res.end();
111 | });
112 |
113 | suica.get("/register.js", async (_ctx, _req, res) => {
114 | const sw = await readAssets("register.js");
115 | res.setHeader("Content-Type", "application/javascript");
116 | res.setHeader("Cache-Control", "private, no-store, must-revalidate");
117 | res.write(sw);
118 | res.end();
119 | });
120 |
121 | for (const [_, list] of Object.entries(articles)) {
122 | list.forEach((article) => {
123 | suica.get(`/blog/${article.key}`, async (_ctx, _req, res) => {
124 | const styleCtx = createStyleContext().set();
125 | const nonce = randomBytes(2).toString("base64");
126 | const body = renderToString(await article.renderer(nonce));
127 | const style = renderToStyleString(styleCtx);
128 | const extendsHead = (
129 | <>
130 |
131 | {await article.extendsHeader()}
132 | >
133 | );
134 | const html = renderToString(
135 |
136 | {body}
137 | ,
138 | );
139 | styleCtx.remove();
140 | res.setHeader("Content-Security-Policy", csp(nonce));
141 | res.setHeader("Content-Type", "text/html");
142 | res.setHeader("Cache-Control", `public, max-age=${60 * 60}`);
143 | res.write(`${html}`);
144 | res.end();
145 | });
146 | });
147 | }
148 |
149 | suica.get("/", async (_ctx, _req, res) => {
150 | const Blog = await import("./pages/blog").then((r) => r.Blog);
151 | const styleCtx = createStyleContext().set();
152 | const nonce = randomBytes(2).toString("base64");
153 | const body = renderToString();
154 | const style = renderToStyleString(styleCtx);
155 | const html = renderToString(
156 |
157 | {body}
158 | ,
159 | );
160 | styleCtx.remove();
161 |
162 | res.setHeader("Content-Security-Policy", csp(nonce));
163 | res.setHeader("Content-Type", "text/html");
164 | res.setHeader("Cache-Control", `public, max-age=${60 * 60}`);
165 | res.write(`${html}`);
166 | res.end();
167 | });
168 |
169 | const main = async () => {
170 | const itr: RequestEventIterator = on(createServer().listen(3000), "request");
171 |
172 | let n = await itr.next();
173 | while (!n.done) {
174 | const [req, res] = n.value;
175 | await suica.run(req, res);
176 | n = await itr.next();
177 | }
178 | };
179 |
180 | main().catch((err) => {
181 | console.error(err);
182 | });
183 |
--------------------------------------------------------------------------------
/packages/lemon/src/jsx.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @see https://www.w3.org/TR/html52/dom.html#global-attributes-2
3 | * @description
4 | * GlobalAttributes types based on html5 spec
5 | * W3C Semantics, structure, and APIs of HTML documents
6 | * 3.2.5.
7 | */
8 |
9 | import { AutoComplete } from "./autocomplete";
10 | import { RFC2978 } from "./rfc2978";
11 | import { Role } from "./role";
12 | import { WAIAria } from "./wai-aria";
13 |
14 | type WhatWGMicroData = Partial<{
15 | itemid: string;
16 | itemprop: string;
17 | itemref: string;
18 | itemscope: string;
19 | itemtype: string;
20 | }>;
21 |
22 | export type GlobalAttributes = Partial<{
23 | accesskey: string;
24 | contenteditable: string;
25 | dir: string;
26 | draggable: string;
27 | hidden: string;
28 | id: string;
29 | lang: string;
30 | spellcheck: string;
31 | style: string;
32 | tabindex: string;
33 | title: string;
34 | translate: string;
35 | class: string;
36 | }> &
37 | WhatWGMicroData &
38 | WAIAria &
39 | Partial<{ role: Role }>;
40 |
41 | export interface InternalIntrinsicElements {
42 | // メインルート
43 | html: GlobalAttributes &
44 | Partial<{
45 | manifest: string;
46 | }>;
47 |
48 | // 文書メタデータ
49 | base: GlobalAttributes &
50 | Partial<{
51 | href: string;
52 | target: string;
53 | }>;
54 | head: GlobalAttributes;
55 | link: GlobalAttributes &
56 | Partial<{
57 | href: string;
58 | crossorigin: string;
59 | rel: string;
60 | media: string;
61 | integrity: string;
62 | hreflang: string;
63 | type: string;
64 | referrerpolicy: string;
65 | sizes: string;
66 | imagesrcset: string;
67 | imagesizes: string;
68 | as: string;
69 | color: string;
70 | disable: boolean;
71 | }>;
72 | meta: GlobalAttributes &
73 | Partial<{
74 | name: string;
75 | "http-equiv": string;
76 | content: string;
77 | charset: RFC2978;
78 | property: string
79 | }>;
80 | style: GlobalAttributes &
81 | Partial<{
82 | media: string;
83 | title: string;
84 | nonce: string;
85 | }>;
86 | title: GlobalAttributes;
87 |
88 | // セクショニングルート
89 | body: GlobalAttributes;
90 |
91 | // コンテンツセクショニング
92 | address: GlobalAttributes;
93 | article: GlobalAttributes;
94 | aside: GlobalAttributes;
95 | footer: GlobalAttributes;
96 | header: GlobalAttributes;
97 | h1: GlobalAttributes;
98 | h2: GlobalAttributes;
99 | h3: GlobalAttributes;
100 | h4: GlobalAttributes;
101 | h5: GlobalAttributes;
102 | h6: GlobalAttributes;
103 | hgroup: GlobalAttributes;
104 | main: GlobalAttributes;
105 | nav: GlobalAttributes;
106 | section: GlobalAttributes;
107 |
108 | // テキストコンテンツ
109 | blockquote: GlobalAttributes &
110 | Partial<{
111 | cite: string;
112 | }>;
113 | dd: GlobalAttributes;
114 | dl: GlobalAttributes;
115 | dt: GlobalAttributes;
116 | div: GlobalAttributes;
117 | figcaption: GlobalAttributes;
118 | figure: GlobalAttributes;
119 | hr: GlobalAttributes;
120 | li: GlobalAttributes &
121 | Partial<{
122 | value: number;
123 | }>;
124 | ol: GlobalAttributes &
125 | Partial<{
126 | reversed: boolean;
127 | start: number;
128 | type: string;
129 | }>;
130 | p: GlobalAttributes;
131 | pre: GlobalAttributes;
132 | ul: GlobalAttributes;
133 |
134 | // インライン文字列意味付け
135 | a: GlobalAttributes &
136 | Partial<{
137 | href: string;
138 | target: string;
139 | download: string;
140 | ping: string;
141 | rel: string;
142 | hreflang: string;
143 | type: string;
144 | }>;
145 | abbr: GlobalAttributes &
146 | Partial<{
147 | title: string;
148 | }>;
149 | b: GlobalAttributes;
150 | bdi: GlobalAttributes &
151 | Partial<{
152 | dir: "ltr" | "rtl" | "auto";
153 | }>;
154 | bdo: GlobalAttributes &
155 | Partial<{
156 | dir: "ltr" | "rtl" | "auto";
157 | }>;
158 | br: GlobalAttributes;
159 | cite: GlobalAttributes;
160 | code: GlobalAttributes;
161 | data: GlobalAttributes &
162 | Partial<{
163 | value: string;
164 | }>;
165 | dfn: GlobalAttributes &
166 | Partial<{
167 | title: string;
168 | }>;
169 | em: GlobalAttributes;
170 | i: GlobalAttributes;
171 | kbd: GlobalAttributes;
172 | mark: GlobalAttributes;
173 | q: GlobalAttributes &
174 | Partial<{
175 | cite: string;
176 | }>;
177 | rb: GlobalAttributes;
178 | rp: GlobalAttributes;
179 | rt: GlobalAttributes;
180 | rtc: GlobalAttributes;
181 | ruby: GlobalAttributes;
182 | s: GlobalAttributes;
183 | samp: GlobalAttributes;
184 | small: GlobalAttributes;
185 | span: GlobalAttributes;
186 | strong: GlobalAttributes;
187 | sub: GlobalAttributes;
188 | sup: GlobalAttributes;
189 | time: GlobalAttributes &
190 | Partial<{
191 | datetime: string;
192 | }>;
193 | u: GlobalAttributes;
194 | var: GlobalAttributes;
195 | wbr: GlobalAttributes;
196 |
197 | // 画像とマルチメディア
198 | area: GlobalAttributes &
199 | Partial<{
200 | alt: string;
201 | coords: string;
202 | shape: string;
203 | href: string;
204 | target: string;
205 | download: string;
206 | ping: string;
207 | rel: string;
208 | referrerpolicy: string;
209 | }>;
210 | audio: GlobalAttributes &
211 | Partial<{
212 | src: string;
213 | crossorigin: string;
214 | preload: string;
215 | autoplay: string;
216 | loop: string;
217 | muted: string;
218 | controls: string;
219 | }>;
220 | img: GlobalAttributes &
221 | Partial<{
222 | alt: string;
223 | src: string;
224 | srcset: string;
225 | sizes: string;
226 | crossorigin: string;
227 | usemap: string;
228 | ismap: string;
229 | width: string;
230 | height: string;
231 | referrerpolicy: string;
232 | decoding: string;
233 | loading: string;
234 | }>;
235 | map: GlobalAttributes &
236 | Partial<{
237 | name: string;
238 | }>;
239 | track: GlobalAttributes &
240 | Partial<{
241 | kind: string;
242 | src: string;
243 | srclang: string;
244 | label: string;
245 | default: string;
246 | }>;
247 | video: GlobalAttributes &
248 | Partial<{
249 | src: string;
250 | crossorigin: string;
251 | poster: string;
252 | preload: string;
253 | autoplay: string;
254 | playsinline: string;
255 | loop: string;
256 | muted: string;
257 | controls: string;
258 | width: string;
259 | height: string;
260 | }>;
261 |
262 | // 埋め込みコンテンツ
263 | embed: GlobalAttributes &
264 | Partial<{
265 | src: string;
266 | type: string;
267 | width: string;
268 | height: string;
269 | }>;
270 | iframe: GlobalAttributes &
271 | Partial<{
272 | src: string;
273 | srcdoc: string;
274 | name: string;
275 | sandbox: string;
276 | allow: string;
277 | allowfullscreen: string;
278 | width: string;
279 | height: string;
280 | referrerpolicy: string;
281 | loading: string;
282 | }>;
283 | object: GlobalAttributes &
284 | Partial<{
285 | data: string;
286 | type: string;
287 | name: string;
288 | usemap: string;
289 | form: string;
290 | width: string;
291 | height: string;
292 | }>;
293 | param: GlobalAttributes &
294 | Partial<{
295 | name: string;
296 | value: string;
297 | }>;
298 | picture: GlobalAttributes;
299 | source: GlobalAttributes &
300 | Partial<{
301 | src: string;
302 | type: string;
303 | srcset: string;
304 | sizes: string;
305 | media: string;
306 | }>;
307 |
308 | // スクリプティング
309 | canvas: GlobalAttributes &
310 | Partial<{
311 | width: string;
312 | height: string;
313 | }>;
314 | noscript: GlobalAttributes;
315 | script: GlobalAttributes &
316 | Partial<{
317 | src: string;
318 | type: string;
319 | nomodule: boolean;
320 | async: boolean;
321 | defer: boolean;
322 | nonce: string;
323 | crossorigin: string;
324 | integrity: string;
325 | referrerpolicy: string;
326 | }>;
327 |
328 | // 編集範囲の特定
329 | del: GlobalAttributes &
330 | Partial<{
331 | cite: string;
332 | datetime: string;
333 | }>;
334 | ins: GlobalAttributes &
335 | Partial<{
336 | cite: string;
337 | datetime: string;
338 | }>;
339 |
340 | // table
341 | caption: GlobalAttributes;
342 | col: GlobalAttributes & Partial<{ span: number }>;
343 | colgroup: GlobalAttributes & Partial<{ span: number }>;
344 | table: GlobalAttributes;
345 | tbody: GlobalAttributes;
346 | thead: GlobalAttributes;
347 | tfoot: GlobalAttributes;
348 | td: GlobalAttributes &
349 | Partial<{
350 | colspan: string;
351 | rowspan: string;
352 | headers: string;
353 | }>;
354 | th: GlobalAttributes &
355 | Partial<{
356 | colspan: string;
357 | rowspan: string;
358 | headers: string;
359 | scope: string;
360 | abbr: string;
361 | }>;
362 | tr: GlobalAttributes;
363 |
364 | // form
365 | button: GlobalAttributes &
366 | Partial<{
367 | disabled: string;
368 | form: string;
369 | formaction: string;
370 | formenctype: string;
371 | formmethod: string;
372 | formnovalidate: string;
373 | formtarget: string;
374 | name: string;
375 | type: string;
376 | value: string;
377 | }>;
378 | datalist: GlobalAttributes;
379 | fieldset: GlobalAttributes &
380 | Partial<{
381 | disabled: string;
382 | form: string;
383 | name: string;
384 | }>;
385 | form: GlobalAttributes &
386 | Partial<{
387 | "accept-charset": RFC2978;
388 | action: string;
389 | autocomplete: AutoComplete;
390 | enctype: string;
391 | method: string;
392 | name: string;
393 | novalidate: string;
394 | target: string;
395 | rel: string;
396 | }>;
397 | input: GlobalAttributes &
398 | Partial<{
399 | accept: string;
400 | alt: string;
401 | autocomplete: AutoComplete;
402 | checked: string;
403 | dirname: string;
404 | disabled: boolean;
405 | form: string;
406 | formaction: string;
407 | formenctype: string;
408 | formmethod: string;
409 | formnovalidate: string;
410 | formtarget: string;
411 | height: string;
412 | list: string;
413 | max: string;
414 | maxlength: string;
415 | min: string;
416 | minlength: string;
417 | multiple: string;
418 | name: string;
419 | pattern: string;
420 | placeholder: string;
421 | readonly: boolean;
422 | required: boolean;
423 | size: string;
424 | src: string;
425 | step: string;
426 | type: string;
427 | value: string;
428 | width: string;
429 | }>;
430 | label: GlobalAttributes &
431 | Partial<{
432 | for: string;
433 | }>;
434 | legend: GlobalAttributes;
435 | meter: GlobalAttributes &
436 | Partial<{
437 | value: number;
438 | min: number;
439 | max: number;
440 | low: number;
441 | high: number;
442 | optimum: number;
443 | }>;
444 | optgroup: GlobalAttributes &
445 | Partial<{
446 | disabled: boolean;
447 | label: string;
448 | }>;
449 | option: GlobalAttributes &
450 | Partial<{
451 | disabled: boolean;
452 | label: string;
453 | }>;
454 | output: GlobalAttributes &
455 | Partial<{
456 | for: string;
457 | form: string;
458 | name: string;
459 | }>;
460 | progress: GlobalAttributes &
461 | Partial<{
462 | value: number;
463 | max: number;
464 | }>;
465 | select: GlobalAttributes &
466 | Partial<{
467 | autocomplete: AutoComplete;
468 | disabled: boolean;
469 | form: string;
470 | multiple: string;
471 | name: string;
472 | required: boolean;
473 | size: string;
474 | }>;
475 | textarea: GlobalAttributes &
476 | Partial<{
477 | autocomplete: AutoComplete;
478 | cols: number;
479 | dirname: string;
480 | disabled: boolean;
481 | form: string;
482 | maxlength: number;
483 | minlength: number;
484 | name: string;
485 | placeholder: string;
486 | readonly: boolean;
487 | required: boolean;
488 | rows: number;
489 | wrap: number;
490 | }>;
491 |
492 | // interactive
493 | details: GlobalAttributes &
494 | Partial<{
495 | open: boolean;
496 | }>;
497 | dialog: GlobalAttributes &
498 | Partial<{
499 | open: boolean;
500 | }>;
501 | menu: GlobalAttributes;
502 | summary: GlobalAttributes;
503 | }
504 |
505 | export interface FC {
506 | (props: P & { children?: ComponentChildren }): VNode | null;
507 | }
508 |
509 | export type Children = VNode | string | null;
510 | export type ComponentChildren = Children[] | Children;
511 |
512 | export interface VNode {
513 | key?: string | number;
514 | type: FC
| string;
515 | props: P & { children?: ComponentChildren };
516 | }
517 |
--------------------------------------------------------------------------------
/packages/melon/src/pages/blog/20211204/index.tsx:
--------------------------------------------------------------------------------
1 | import { h, FC } from "@kajitsu/lemon";
2 |
3 | const snipet1 = `
4 | let result = exclaim(capitalize(doubleSay("hello")));
5 | result //=> "Hello, hello!"
6 |
7 | let result = "hello"
8 | |> doubleSay
9 | |> capitalize
10 | |> exclaim
11 | `.trim();
12 |
13 | const snipet2 = `
14 | function doubleSay (str) {
15 | return str + ", " + str;
16 | }
17 | async function capitalize (str) {
18 | let response = await
19 | capitalizeOnTheServer(str);
20 | return response.getCapitalized();
21 | }
22 | function exclaim (str) {
23 | return str + '!';
24 | }
25 |
26 | let result = "hello"
27 | |> doubleSay
28 | |> capitalize
29 | |> exclaim;
30 |
31 | // result => "[Object Promise]"
32 | `.trim();
33 |
34 | const snipet3 = `
35 | let result = await ("hello"
36 | |> doubleSay
37 | |> capitalize)
38 | |> exclaim
39 | `.trim();
40 |
41 | const snipet4 = `
42 | let result = "hello"
43 | |> doubleSay
44 | |> await capitalize
45 | |> exclaim;
46 | `.trim();
47 |
48 | const snipet5 = `
49 | let result = "hello"
50 | |> doubleSay
51 | |> (await capitalize)
52 | |> exclaim;
53 | `.trim();
54 |
55 | const snipet6 = `
56 | let result = "hello"
57 | |> doubleSay
58 | |> await
59 | |> capitalize
60 | |> exclaim;
61 | `.trim();
62 |
63 | const snipet7 = `
64 | let promise = "hello"
65 | |> doubleSay
66 | |> await
67 | exclaim(capitalize(promise))
68 | `.trim();
69 |
70 | const snipet8 = `
71 | await
72 | asyncFn()
73 | `.trim();
74 |
75 | const snipet9 = `
76 | let promise = "hello"
77 | |> doubleSay
78 | |> await;
79 | exclaim(capitalize(promise))
80 | `.trim();
81 |
82 | const snipet10 = `
83 | let promise = "hello"
84 | |> doubleSay
85 | |> await
86 | exclaim(capitalize(promise));
87 | `.trim();
88 |
89 | const snipet11 = `
90 | // Basic Usage
91 | x |>> f //--> f(x)
92 | x |> f(^) //--> f(x)
93 |
94 | x |>> f(y) //--> f(y)(x)
95 | x |> f(y)(^) //--> Syntax Error
96 |
97 | // 2+ Arity Usage
98 | x |> f(^, 10) //--> f(x,10)
99 |
100 | // Async solution (does not require special casing)
101 | x |> await f(^) //--> await f(x)
102 | x |> await f(^) |> g //--> g(await f(x))
103 |
104 | // Other expressions
105 | f(x) |> ^.data //--> f(x).data
106 | f(x) |> ^[^.length-1] //--> let temp=f(x), temp[temp.length-1]
107 | f(x) |> { result: ^ } //--> { result: f(x) }
108 | `.trim();
109 |
110 | const snipet12 = `
111 | // Basic Usage
112 | x |> f //--> f(x)
113 | x |> f(^) //--> f(x)
114 |
115 | // 2+ Arity Usage
116 | x |> f(y) //--> Syntax Error
117 | x |> f(y, ^) //--> f(y, x)
118 | x |> f(^, y) //--> f(x, y)
119 | x |> f(y)(^) //--> f(y)(x)
120 |
121 | // Async Solution (Note this would not require special casing)
122 | x |> await f(^) //--> await f(x)
123 | x |> await f(^) |> g //--> g(await f(x))
124 |
125 | // Arbitrary Expressions
126 | f(x) |> ^.data //--> f(x).data
127 | f(x) |> ^[^.length-1] //--> let temp=f(x), temp[temp.length-1]
128 | f(x) |> { result: ^ } //--> { result: f(x) }
129 | `.trim();
130 |
131 | const snipet13 = `
132 | value |> fn(10, ?)
133 | `.trim();
134 |
135 | const snipet14 = `
136 | value |> fn(10, ? ? cond1(?) : defaultValue)
137 | `.trim();
138 |
139 | const snipet15 = `
140 | value |> fn(10, ? ?? defaultValue)
141 | `.trim();
142 |
143 | const snipet16 = `
144 | value |> fn(10, ? ?? ? ? ? : ?)
145 | ~ ~~ ~ ~ ~ ~ ~
146 | | |_|_|_|_|_|_______ nullish coalescing operator
147 | | | | | | |
148 | |____._|_._|_.____ placeholder of pipeline
149 | | |
150 | |___.___ conditional ternary operator
151 | `.trim();
152 |
153 | const snipet17 = `
154 | value |> fn(20 % %)
155 | `.trim();
156 |
157 | export const Article: FC<{ nonce?: string }> = ({ nonce }) => (
158 |
159 |
160 | tc39/proposal-pipeline-operator の
161 | 過去と現状
162 |
163 |
164 | pipeline-operator の ドラフトを
165 |
166 | 振り返る
167 |
168 |
169 | tc39/proposal-pipeline-operator の
170 | リポジトリができた時点の,
171 | (厳密には Stage-1 になった時点) pipeline operator の syntax を
172 | 簡単におさらいしてみましょう.
173 |
174 |
175 |
176 | {snipet1}
177 |
178 |
179 | F# や Elixir などに触れているひとは,
180 |
181 | 馴染み深いものかもしれません.
182 |
183 | その他の,
184 |
185 | 関数型プログラミング言語では,
186 |
187 | 記法や振る舞いに若干の差異はあるものの,
188 |
189 | 類似の
190 | pipeline があると思います.
191 |
192 |
193 |
194 | c(b(a)) のような関数呼び出しがネストしたケースで,
195 |
196 | a |> b |> c と,
197 |
198 | より直感的に書けることができるようになります.
199 |
200 |
201 |
202 | この仕様としては,
203 |
204 | このオペレーターは,
205 |
206 | 優先度は , より強く,
207 |
208 | その他のオペレーターより弱いです.
209 |
210 | そして,
211 |
212 | Unary-Function 前提で考えられていました.
213 | 上記のような pipeline は,
214 |
215 | これより F#-style pipeline として扱われていきます.
216 |
217 |
218 | 長い期間 Stage-1 にとどまり続けた理由
219 |
220 | pipeline-operator は 2017年に Stage-1 になってから 三年間 Stage-2 に上がることはありませんでした.
221 |
222 | この記事では,
223 |
224 | なぜ上がるのが難しかったかに焦点を
225 |
226 | 当てて紹介していきます.
227 |
228 |
229 | F#-style pipeline における ASI との競合
230 |
231 | 非同期を
232 | pipeline で扱いたいモチベーションから ASI で起きた予期せぬ問題までの流れを
233 | コードと順に追っていきます.
234 |
235 |
236 |
237 | {snipet2}
238 |
239 |
240 | 普通に pipeline を
241 | 活用すると,
242 | 途中に promise を
243 | 返す処理があった場合,
244 | その promise が解決されずに次の処理に渡されてしまいます.
245 | これを
246 |
247 | 解決しようとすると,
248 |
249 |
250 | {snipet3}
251 |
252 |
253 | この様になってしまいます.
254 |
255 | しかし,
256 |
257 | これはあまり直感的ではないです それを
258 |
259 | 解決するために pipeline 下での await を
260 | 許すとこのように書けるようになります.
261 |
262 |
263 |
264 | {snipet4}
265 |
266 |
267 | ここで新たに生じた問題が,
268 |
269 | 上のコードと下のコードで全く別の
270 |
271 | 意味になってしまうということです.
272 |
273 |
274 |
275 | {snipet5}
276 |
277 |
278 | その問題を
279 |
280 | 回避するために,
281 |
282 | |> await という構文が提案されました.
283 |
284 |
285 |
286 | {snipet6}
287 |
288 |
289 | |> await という構文を
290 |
291 | 導入しようとしたときに ASI の問題が生じました
292 |
293 |
294 | {snipet7}
295 |
296 |
297 | 既存の async / await では,
298 |
299 | await 前の改行が許されており,
300 |
301 |
302 |
303 | {snipet8}
304 |
305 |
306 | と記述することができます.
307 | ここから,
308 |
309 |
310 |
311 | {snipet9}
312 |
313 |
314 | {snipet10}
315 |
316 |
317 | 上記のどちらと判断すればよいかわからず,
318 |
319 | ASI がどこにセミコロンを挿入すべきか判断がつかなくなってしまうという問題が
320 |
321 | 起きました. それらを回避するために await |> や |> await |> なども
322 |
323 | 考えられましたが,(|> await で終わる pipeline を許さないための構文) 結果的に,
324 |
325 | これを解決する画期的な方法がなく,
326 |
327 | 仕様としては,
328 |
329 | await を含むパターンを後回しにし,
330 |
331 | それが要因の一つとなり,
332 |
333 | Stage-2 へ上げる判断はなされませんでした.
334 |
335 |
336 | await の問題を解決するために,
337 |
338 | 過去に drop された hack style の
339 |
340 | 復活が提案される
341 |
342 |
343 | pipeline operator 下で,
344 |
345 | await を扱う良い方法がない現状を
346 |
347 | 打破するために,
348 |
349 | 2015年 に考えられていた hack style を
350 |
351 | 復活させる提案がされました.
352 |
353 | それを受けて,
354 |
355 | 2018年に F# style と hack style を
356 | 混合させた提案が2つできました.
357 |
358 | それは,
359 | {" "}
360 |
361 | "split-mix pipes"
362 | {" "}
363 | と
364 |
365 | "smart-mix pipes" (proposal-smart-pipeline)
366 | {" "}
367 | です.
368 |
369 |
370 |
371 | 2つに共通している点は,
372 |
373 | Placeholder を
374 | 定義し,
375 |
376 | その token を
377 | 活用することで,
378 |
379 | await, yield を
380 | 含んだケースを
381 |
382 | 対応しつつ, 前の処理で評価した結果を
383 | ,
384 | 次の処理の関数の任意の場所に適用できるというものでした.
385 |
386 |
387 |
388 | split-mix は |> と |>> (厳密には,
389 |
390 | どの文字を
391 |
392 | 使うかまでは決まっていない) 2つのオペレーターに分割しているのが特徴です.
393 |
394 | smart-mix は 関数を
395 |
396 | そのまま渡すか,
397 |
398 | Placeholder によってどこに適用するか,
399 |
400 | 文法を
401 |
402 | 限定するというものでした
403 |
404 |
407 |
408 | {snipet11}
409 |
410 |
411 | split-mix は,
412 |
413 | (導入されたときに)各ブラウザが,
414 |
415 | 2つのオペレーターを
416 |
417 | 実装する必要があることから,
418 |
419 | これに賛同する人はいませんでした.
420 |
421 |
422 |
425 |
426 | {snipet12}
427 |
428 |
429 | pipeline-operator を Stage-2 に上げるために再挑戦
430 |
431 |
432 | 2018年3月に,
433 |
434 | pipeline-operator を Stage-2 に上げるために F# pipeline と smart-mix pipeline のそれぞれを
435 |
436 | 一緒に再度提案し tc39 に持ち込みました.
437 |
438 | しかし,
439 |
440 | どちらの
441 |
442 | 提案も賛同を
443 |
444 | 得ることはなく,
445 |
446 | 一部からは構文上の課題からこれを
447 |
448 | 標準化する価値はないオペレーターだと述べられました.
449 |
450 |
451 |
452 | 2018年7月には,
453 |
454 | pipeline-operator にも 関係がある partial-function-application (PFA) を Stage-2
455 | に進めるべく提案されました.
456 | これに関しても,
457 |
458 | 一部から反対され続けられています.
459 | Google V8 チームからは,
460 |
461 | 開発者が PFA を
462 | 通して 容易にたくさんのクロージャ定義することでメモリが逼迫することを
463 |
464 | 懸念していました.
465 |
466 |
467 |
468 | これにより,
469 |
470 | PFA が stage-2 に進めないこともあって,
471 |
472 | pipeline-operator の Stage は上がることはありませんでした.
473 |
474 |
475 |
476 | @codehag 氏(Mozilla SpiderMonkey チーム)は,
477 |
478 | pipe に関するユーザー調査を
479 |
480 | 主導していました.
481 | その結果,
482 |
483 | smart-mix pipeline が若干好まれているようでした.
484 |
485 | しかし,
486 |
487 | F# pipeline の方が(実装時の)エラーが少なかったようです.
488 |
489 | 結論としては,
490 |
491 | この結果はどちらが優位であるかを
492 |
493 | 決めるほどの優位な差はないというものでした.
494 |
495 | これらを
496 |
497 | 総合的に踏まえて,
498 |
499 | Mozilla SpiderMonkey チーム はそこまで,
500 |
501 | pipeline operator の二つの提案に対して前向きではありませんでした.
502 |
503 |
504 |
505 | ここから三年間に渡り,
506 |
507 | これら二つの pipeline operater が 同意を
508 |
509 | 得られるための最善について オフライン / オンラインで議論されます.
510 |
511 |
512 | 2021年 pipeline-operator が Stage-2 に上がるにあたって起きたこと
513 | State of JS 2020 にて pipeline operator が話題に上がる
514 |
515 | 2021年1月に State of JS 2020 にて,
516 |
517 | 「最も JavaScript に足りないものはなにか」 という設問において pipeline operator が四位にランクインしたという報告を
518 |
519 | 受けました.
520 |
521 |
522 |
523 | これがきっかけの1つとなったかどうかはわかりませんが,
524 |
525 | 結果として三年間大きな動きがなかったこの proposal に動きが出てきました.
526 |
527 |
528 |
529 |
536 |
537 |
538 |
539 | https://2020.stateofjs.com/ja-JP/opinions/ より
540 |
541 |
542 |
543 | 2021年3月に,
544 |
545 | これまで チャンピオンを
546 |
547 | 努めていた @littledan 氏 (Igalia) が,
548 |
549 | 他の project で時間が割けない状況であるということから,
550 | この proposal のチャンピオンが @tabatkins 氏(Google), @rbuckton 氏 (Microsoft) が
551 | 共同チャンピオンとして共に引き継ぐことに同意しました.
552 |
553 |
554 |
555 | また,
556 |
557 | @rbuckton (Microsoft) 氏 が これまで上がっている F#-style pipeline, hack-style pipeline, smart-mix pipeline,
558 | 三つを
559 |
560 | 比較した結果を
561 | Gist にまとめ
562 | ,
563 | 「ほんの些細な違い」と結論づけています.
564 | この,
565 |
566 | Gist での議論もとても白熱しています.
567 |
568 |
569 |
570 | この,
571 |
572 | Gist に刺激を
573 |
574 | 受けた smart-mix pipeline のドラフトを
575 |
576 | 書いている @js-choi 氏 (Indiana University)は,
577 |
578 | smart-mix pipeline のドラフトではなく,
579 | Hack-style pipeline を
580 | 書くことにシフトしました.
581 |
582 | それにより,
583 |
584 | smart-mix pipeline は Hack-style pipeline と併合された扱いになり 取り下げられました.
585 |
586 |
587 | 再度 Stage-2 に向けて
588 |
589 | 再度,
590 |
591 | Stage-2 に向けていくつかの pipeline-operateor の style を
592 | 提示に向けて議論が行われていました.
593 |
594 | その結果,
595 |
596 | Hack-style pipeline を
597 | 持ち込む事になりました.
598 | それにあたり,
599 |
600 | F#-style がいつまでも行き詰まっていることから,
601 |
602 | Hack-style pipeline に仮合意することなりました.
603 |
604 |
605 |
606 | 8月31日の正式な委員会で,
607 |
608 | プレナリーが実施されました.
609 |
610 | @tabatkins 氏(Google)が Hack-style pipeline を,
611 | 現状のチャンピオンの暫定的ななコンセンサスとして提示し,
612 |
613 | Stage-2 に進めることを
614 |
615 | 提案しました.
616 |
617 |
618 |
619 | それに対して,
620 |
621 |
622 |
623 | - 他の proposal である bind-operator と将来的に衝突してしまうのではないか
624 | -
625 | @codehag 氏 (Mozilla SpiderMonkey) はいずれにしても pipeline には若干後ろ向き,
626 |
627 | ただし Stage-2 を
628 | ブロックするほどではない
629 |
630 |
631 |
632 | という指摘がなされました.
633 |
634 |
635 |
636 | これ以外に,
637 |
638 | 特に指摘もなかったため{" "}
639 |
640 | pipeline-opearator は Stage-2 に進むことになりました.
641 |
642 |
643 |
644 |
645 | 備考として,
646 |
647 | 以前 Google V8 チームから挙げられていた,
648 |
649 | pipe と PFA を
650 | 用いたときに,
651 |
652 | メモリを
653 |
654 | 逼迫する可能性についての異論はありませんでした.
655 |
656 |
657 |
658 | 仕様を
659 |
660 | 固めるために Placeholder の トークンを
661 |
662 | 決める
663 |
664 |
665 | 2021年10月末から議論されているトピックとして,
666 |
667 | Placeholder の token に何を
668 |
669 | 用いるかというものがあります.
670 |
671 |
672 |
673 | 以前は ? が提案されていました,
674 | 現在{" "}
675 |
676 | Bikeshedding the Hack topic token #91
677 | {" "}
678 | にて 最終的に,
679 |
680 | Placeholder の token に何を
681 |
682 | 用いるか議論されています.
683 |
684 |
685 |
686 | ? が抱えている問題
687 |
688 |
689 | ? を token としたときに次のようなコードになります.
690 |
691 |
692 |
693 | {snipet13}
694 |
695 |
696 | これを
697 | ,
698 | 三項演算子 や Nullish coalescing operator と組み合わせたときに,
699 |
700 | 非常に混乱を
701 |
702 | 招くコードになってしまう問題があります
703 |
704 |
705 | {snipet14}
706 |
707 |
708 | {snipet15}
709 |
710 |
711 | また,
712 |
713 | これを
714 |
715 | 許すと,
716 |
717 | 構文的には下のコードがあり得ることになります
718 |
719 |
720 | {snipet16}
721 |
722 |
723 | % が抱えている問題
724 |
725 |
726 | % も ? に近い課題を
727 |
728 | 抱えています.
729 |
730 | % 単体で オペレーターとしてすでにあるので
731 |
732 |
733 | {snipet17}
734 |
735 |
736 | のようなコードが発生してしまいます,
737 |
738 | これを
739 |
740 | 文脈から pipline-operator 下の placeholder か,
741 |
742 | 剰余を
743 |
744 | 求める % operator か どうか判断するのは非常に困難です.
745 |
746 | # でも同様に,
747 |
748 | クラスのメソッド内で pipeline が使われたとき,
749 |
750 | private field か placeholder の token か,
751 |
752 | あるいは 現在上がっている proposal の tuple か 非常に解析が難しくなってしまいます.
753 |
754 |
755 |
756 | これらのことから,
757 |
758 | 独立した token が良いのではないかという意見もあります.
759 |
760 |
761 |
762 | ## や (), $_, @, ^ など 様々なアイデアが github
763 | 上 の issue で 議論されています.
764 |
765 | そして,
766 |
767 | まだこれといった token は決まっていません.
768 | ($, _ 及びそれらの組み合わせは,
769 |
770 | 既存のコードを
771 |
772 | 破壊する可能性があるのでなさそうです)
773 |
774 |
775 | Stage-3 の条件としては,
776 |
777 | 仕様として完成している必要があるので,
778 |
779 | まずここが決まらないと pipeline-operator の Stage-3 は難しいでしょう.
780 |
781 |
782 | 現状の Hack-style pipeline の仕様
783 |
784 | 最後に,
785 |
786 | これらを
787 |
788 | 経てどのような proposal になったか見てみましょう.
789 | Plfaceholder が 仮に % になれば以下のようになります
790 |
791 |
792 | -
793 |
value |> foo(%) for unary function calls,
794 |
795 | -
796 |
value |> foo(1, %) for n-ary function calls,
797 |
798 | -
799 |
value |> %.foo() for method calls,
800 |
801 | -
802 |
value |> % + 1 for arithmetic,
803 |
804 | -
805 |
value |> [%, 0] for array literals,
806 |
807 | -
808 |
value |> {foo: %} for object literals,
809 |
810 | -
811 |
value |> \`${%}\`` for template literals,
812 |
813 | -
814 |
value |> new Foo(%) for constructing objects,
815 |
816 | -
817 |
value |> await % for awaiting promises,
818 |
819 | -
820 |
value |> (yield %) for yielding generator values,
821 |
822 | -
823 |
value |> import(%) for calling function-like keywords,
824 |
825 |
826 | おわりに
827 |
828 | この記事では,
829 |
830 | pipeline-operator が Stage-1 から Stage-2 に上がるまでを
831 |
832 | 紹介しました.
833 |
834 |
835 |
836 | 東京Node学園祭'2018 にて,
837 |
838 | tc39 に所属している daniel さんが来日し,
839 |
840 | 発表+参加者とのディスカッションに参加したときに 漠然と,
841 |
842 | 「JavaScript に新しい syntax を
843 | 導入するのは難しいのだなあ」と感じていたました.
844 | 今回,
845 |
846 | この記事を
847 |
848 | 書くにあたって pipeline-operator について調べ,
849 |
850 | より強くその難しさを
851 |
852 | 実感できたような気がします.
853 |
854 |
855 |
856 | pipeline-operator や partial-function-application, pattern-matching, bind-operator などの仕様が JavaScript
857 | に実装されたときには,
858 |
859 | とても楽しくプログラミングができそうで楽しみです.
860 |
861 |
862 | 参考文献
863 |
872 |
873 |
874 | );
875 |
876 | const lazyload = `
877 | window.addEventListener("DOMContentLoaded", function() {
878 | var images = [].slice.call(document.querySelectorAll("img"));
879 | if ("IntersectionObserver" in window) {
880 | var lazyImageObserver = new IntersectionObserver(function(entries, observer) {
881 | entries.forEach(function(entry) {
882 | if (entry.isIntersecting) {
883 | var lazyImage = entry.target;
884 | lazyImage.src = lazyImage.dataset.src;
885 | lazyImageObserver.unobserve(lazyImage);
886 | }
887 | });
888 | });
889 | images.forEach(function(image) {
890 | lazyImageObserver.observe(image, { threshold: 0.2 });
891 | });
892 | } else {
893 | images.forEach(function(img) { img.src = img.dataset.src; })
894 | }
895 | }, false)
896 | `;
897 |
--------------------------------------------------------------------------------