@b9g/crank 0.5.0-beta.4 → 0.5.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/umd.js CHANGED
@@ -1812,6 +1812,429 @@
1812
1812
  return result;
1813
1813
  }
1814
1814
 
1815
+ const cache = new Map();
1816
+ function jsx(spans, ...expressions) {
1817
+ const key = JSON.stringify(spans.raw);
1818
+ let parseResult = cache.get(key);
1819
+ if (parseResult == null) {
1820
+ parseResult = parse(spans.raw);
1821
+ cache.set(key, parseResult);
1822
+ }
1823
+ const { element, targets } = parseResult;
1824
+ for (let i = 0; i < expressions.length; i++) {
1825
+ const exp = expressions[i];
1826
+ const target = targets[i];
1827
+ if (target) {
1828
+ if (target.type === "error") {
1829
+ throw new SyntaxError(target.message.replace("${}", formatTagForError(exp)));
1830
+ }
1831
+ target.value = exp;
1832
+ }
1833
+ }
1834
+ return build(element);
1835
+ }
1836
+ /**
1837
+ * Matches first significant character in children mode.
1838
+ *
1839
+ * Group 1: newline
1840
+ * Group 2: comment
1841
+ * Group 3: tag
1842
+ * Group 4: closing slash
1843
+ * Group 5: tag name
1844
+ *
1845
+ * The comment group must appear first because the tag group can potentially
1846
+ * match a comment, so that we can handle tag expressions where we’ve reached
1847
+ * the end of a span.
1848
+ */
1849
+ const CHILDREN_RE = /((?:\r|\n|\r\n)\s*)|(<!--[\S\s]*?(?:-->|$))|(<\s*(\/{0,2})\s*([-_$\w]*))/g;
1850
+ /**
1851
+ * Matches props after element tags.
1852
+ *
1853
+ * Group 1: tag end
1854
+ * Group 2: spread props
1855
+ * Group 3: prop name
1856
+ * Group 4: equals
1857
+ * Group 5: prop value string
1858
+ */
1859
+ const PROPS_RE = /\s*(?:(\/?\s*>)|(\.\.\.\s*)|(?:([-_$\w]+)\s*(=)?\s*(?:("(\\"|[\S\s])*?(?:"|$)|'(?:\\'|[\S\s])*?(?:'|$)))?))/g;
1860
+ const CLOSING_BRACKET_RE = />/g;
1861
+ const CLOSING_SINGLE_QUOTE_RE = /[^\\]?'/g;
1862
+ const CLOSING_DOUBLE_QUOTE_RE = /[^\\]?"/g;
1863
+ const CLOSING_COMMENT_RE = /-->/g;
1864
+ function parse(spans) {
1865
+ let matcher = CHILDREN_RE;
1866
+ const stack = [];
1867
+ let element = {
1868
+ type: "element",
1869
+ open: { type: "tag", slash: "", value: "" },
1870
+ close: null,
1871
+ props: [],
1872
+ children: [],
1873
+ };
1874
+ const targets = [];
1875
+ let lineStart = true;
1876
+ for (let s = 0; s < spans.length; s++) {
1877
+ const span = spans[s];
1878
+ // Whether or not an expression is upcoming. Used to provide better errors.
1879
+ const expressing = s < spans.length - 1;
1880
+ let expressionTarget = null;
1881
+ for (let i = 0, end = i; i < span.length; i = end) {
1882
+ matcher.lastIndex = i;
1883
+ const match = matcher.exec(span);
1884
+ end = match ? match.index + match[0].length : span.length;
1885
+ switch (matcher) {
1886
+ case CHILDREN_RE: {
1887
+ if (match) {
1888
+ const [, newline, comment, tag, closingSlash, tagName] = match;
1889
+ if (i < match.index) {
1890
+ let before = span.slice(i, match.index);
1891
+ if (lineStart) {
1892
+ before = before.replace(/^\s*/, "");
1893
+ }
1894
+ if (newline) {
1895
+ if (span[Math.max(0, match.index - 1)] === "\\") {
1896
+ // We preserve whitespace before escaped newlines and have to
1897
+ // remove the backslash.
1898
+ // jsx` \
1899
+ // `
1900
+ before = before.slice(0, -1);
1901
+ }
1902
+ else {
1903
+ before = before.replace(/\s*$/, "");
1904
+ }
1905
+ }
1906
+ if (before) {
1907
+ element.children.push({ type: "value", value: before });
1908
+ }
1909
+ }
1910
+ lineStart = !!newline;
1911
+ if (comment) {
1912
+ if (end === span.length) {
1913
+ // Expression in a comment:
1914
+ // jsx`<!-- ${exp} -->`
1915
+ matcher = CLOSING_COMMENT_RE;
1916
+ }
1917
+ }
1918
+ else if (tag) {
1919
+ if (closingSlash) {
1920
+ element.close = {
1921
+ type: "tag",
1922
+ slash: closingSlash,
1923
+ value: tagName,
1924
+ };
1925
+ if (!stack.length) {
1926
+ if (end !== span.length) {
1927
+ throw new SyntaxError(`Unmatched closing tag "${tagName}"`);
1928
+ }
1929
+ // ERROR EXPRESSION
1930
+ expressionTarget = {
1931
+ type: "error",
1932
+ message: "Unmatched closing tag ${}",
1933
+ value: null,
1934
+ };
1935
+ }
1936
+ else {
1937
+ if (end === span.length) {
1938
+ // TAG EXPRESSION
1939
+ expressionTarget = element.close;
1940
+ }
1941
+ element = stack.pop();
1942
+ matcher = CLOSING_BRACKET_RE;
1943
+ }
1944
+ }
1945
+ else {
1946
+ const next = {
1947
+ type: "element",
1948
+ open: {
1949
+ type: "tag",
1950
+ slash: "",
1951
+ value: tagName,
1952
+ },
1953
+ close: null,
1954
+ props: [],
1955
+ children: [],
1956
+ };
1957
+ element.children.push(next);
1958
+ stack.push(element);
1959
+ element = next;
1960
+ matcher = PROPS_RE;
1961
+ if (end === span.length) {
1962
+ // TAG EXPRESSION
1963
+ expressionTarget = element.open;
1964
+ }
1965
+ }
1966
+ }
1967
+ }
1968
+ else {
1969
+ if (i < span.length) {
1970
+ let after = span.slice(i);
1971
+ if (!expressing) {
1972
+ // trim trailing whitespace
1973
+ after = after.replace(/\s*$/, "");
1974
+ }
1975
+ if (after) {
1976
+ element.children.push({ type: "value", value: after });
1977
+ }
1978
+ }
1979
+ }
1980
+ break;
1981
+ }
1982
+ case PROPS_RE: {
1983
+ if (match) {
1984
+ const [, tagEnd, spread, name, equals, string] = match;
1985
+ if (i < match.index) {
1986
+ throw new SyntaxError(`Unexpected text \`${span.slice(i, match.index).trim()}\``);
1987
+ }
1988
+ if (tagEnd) {
1989
+ if (tagEnd[0] === "/") {
1990
+ // This is a self-closing element, so there will always be a
1991
+ // result on the stack.
1992
+ element = stack.pop();
1993
+ }
1994
+ matcher = CHILDREN_RE;
1995
+ }
1996
+ else if (spread) {
1997
+ const value = {
1998
+ type: "value",
1999
+ value: null,
2000
+ };
2001
+ element.props.push(value);
2002
+ // SPREAD PROP EXPRESSION
2003
+ expressionTarget = value;
2004
+ if (!(expressing && end === span.length)) {
2005
+ throw new SyntaxError('Expression expected after "..."');
2006
+ }
2007
+ }
2008
+ else if (name) {
2009
+ let value;
2010
+ if (string == null) {
2011
+ if (!equals) {
2012
+ value = { type: "value", value: true };
2013
+ }
2014
+ else if (end < span.length) {
2015
+ throw new SyntaxError(`Unexpected text \`${span.slice(end, end + 20)}\``);
2016
+ }
2017
+ else {
2018
+ value = { type: "value", value: null };
2019
+ // PROP EXPRESSION
2020
+ expressionTarget = value;
2021
+ if (!(expressing && end === span.length)) {
2022
+ throw new SyntaxError(`Expression expected for prop "${name}"`);
2023
+ }
2024
+ }
2025
+ }
2026
+ else {
2027
+ const quote = string[0];
2028
+ value = { type: "propString", parts: [] };
2029
+ value.parts.push(string);
2030
+ if (end === span.length) {
2031
+ matcher =
2032
+ quote === "'"
2033
+ ? CLOSING_SINGLE_QUOTE_RE
2034
+ : CLOSING_DOUBLE_QUOTE_RE;
2035
+ }
2036
+ }
2037
+ const prop = {
2038
+ type: "prop",
2039
+ name,
2040
+ value,
2041
+ };
2042
+ element.props.push(prop);
2043
+ }
2044
+ }
2045
+ else {
2046
+ if (!expressing) {
2047
+ if (i === span.length) {
2048
+ throw new SyntaxError(`Expected props but reached end of document`);
2049
+ }
2050
+ else {
2051
+ throw new SyntaxError(`Unexpected text \`${span.slice(i, i + 20).trim()}\``);
2052
+ }
2053
+ }
2054
+ // Unexpected expression errors are handled in the outer loop.
2055
+ //
2056
+ // This would most likely be the starting point for the logic of
2057
+ // prop name expressions.
2058
+ // jsx`<p ${name}=${value}>`
2059
+ }
2060
+ break;
2061
+ }
2062
+ case CLOSING_BRACKET_RE: {
2063
+ // We’re in a closing tag and looking for the >.
2064
+ if (match) {
2065
+ if (i < match.index) {
2066
+ throw new SyntaxError(`Unexpected text \`${span.slice(i, match.index).trim()}\``);
2067
+ }
2068
+ matcher = CHILDREN_RE;
2069
+ }
2070
+ else {
2071
+ if (!expressing) {
2072
+ throw new SyntaxError(`Unexpected text \`${span.slice(i, i + 20).trim()}\``);
2073
+ }
2074
+ }
2075
+ break;
2076
+ }
2077
+ case CLOSING_SINGLE_QUOTE_RE:
2078
+ case CLOSING_DOUBLE_QUOTE_RE: {
2079
+ const string = span.slice(i, end);
2080
+ const prop = element.props[element.props.length - 1];
2081
+ const propString = prop.value;
2082
+ propString.parts.push(string);
2083
+ if (match) {
2084
+ matcher = PROPS_RE;
2085
+ }
2086
+ else {
2087
+ if (!expressing) {
2088
+ throw new SyntaxError(`Missing \`${matcher === CLOSING_SINGLE_QUOTE_RE ? "'" : '"'}\``);
2089
+ }
2090
+ }
2091
+ break;
2092
+ }
2093
+ case CLOSING_COMMENT_RE: {
2094
+ if (match) {
2095
+ matcher = CHILDREN_RE;
2096
+ }
2097
+ else {
2098
+ if (!expressing) {
2099
+ throw new SyntaxError("Expected `-->` but reached end of template");
2100
+ }
2101
+ }
2102
+ break;
2103
+ }
2104
+ }
2105
+ }
2106
+ if (expressing) {
2107
+ if (expressionTarget) {
2108
+ targets.push(expressionTarget);
2109
+ if (expressionTarget.type === "error") {
2110
+ break;
2111
+ }
2112
+ continue;
2113
+ }
2114
+ switch (matcher) {
2115
+ case CHILDREN_RE: {
2116
+ const target = { type: "value", value: null };
2117
+ element.children.push(target);
2118
+ targets.push(target);
2119
+ break;
2120
+ }
2121
+ case CLOSING_SINGLE_QUOTE_RE:
2122
+ case CLOSING_DOUBLE_QUOTE_RE: {
2123
+ const prop = element.props[element.props.length - 1];
2124
+ const target = { type: "value", value: null };
2125
+ prop.value.parts.push(target);
2126
+ targets.push(target);
2127
+ break;
2128
+ }
2129
+ case CLOSING_COMMENT_RE:
2130
+ targets.push(null);
2131
+ break;
2132
+ default:
2133
+ throw new SyntaxError("Unexpected expression");
2134
+ }
2135
+ }
2136
+ else if (expressionTarget) {
2137
+ throw new SyntaxError("Expression expected");
2138
+ }
2139
+ lineStart = false;
2140
+ }
2141
+ if (stack.length) {
2142
+ const ti = targets.indexOf(element.open);
2143
+ if (ti === -1) {
2144
+ throw new SyntaxError(`Unmatched opening tag "${element.open.value}"`);
2145
+ }
2146
+ targets[ti] = {
2147
+ type: "error",
2148
+ message: "Unmatched opening tag ${}",
2149
+ value: null,
2150
+ };
2151
+ }
2152
+ if (element.children.length === 1 && element.children[0].type === "element") {
2153
+ element = element.children[0];
2154
+ }
2155
+ return { element, targets };
2156
+ }
2157
+ function build(parsed) {
2158
+ if (parsed.close !== null &&
2159
+ parsed.close.slash !== "//" &&
2160
+ parsed.open.value !== parsed.close.value) {
2161
+ throw new SyntaxError(`Unmatched closing tag ${formatTagForError(parsed.close.value)}, expected ${formatTagForError(parsed.open.value)}`);
2162
+ }
2163
+ const children = [];
2164
+ for (let i = 0; i < parsed.children.length; i++) {
2165
+ const child = parsed.children[i];
2166
+ children.push(child.type === "element" ? build(child) : child.value);
2167
+ }
2168
+ let props = parsed.props.length ? {} : null;
2169
+ for (let i = 0; i < parsed.props.length; i++) {
2170
+ const prop = parsed.props[i];
2171
+ if (prop.type === "prop") {
2172
+ let value;
2173
+ if (prop.value.type === "value") {
2174
+ value = prop.value.value;
2175
+ }
2176
+ else {
2177
+ let string = "";
2178
+ for (let i = 0; i < prop.value.parts.length; i++) {
2179
+ const part = prop.value.parts[i];
2180
+ if (typeof part === "string") {
2181
+ string += part;
2182
+ }
2183
+ else if (typeof part.value !== "boolean" && part.value != null) {
2184
+ string +=
2185
+ typeof part.value === "string" ? part.value : String(part.value);
2186
+ }
2187
+ }
2188
+ value = string
2189
+ // remove quotes
2190
+ .slice(1, -1)
2191
+ // unescape things
2192
+ // adapted from https://stackoverflow.com/a/57330383/1825413
2193
+ .replace(/\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\u\{[0-9a-f]+\}|\\./gi, (match) => {
2194
+ switch (match[1]) {
2195
+ case "b":
2196
+ return "\b";
2197
+ case "f":
2198
+ return "\f";
2199
+ case "n":
2200
+ return "\n";
2201
+ case "r":
2202
+ return "\r";
2203
+ case "t":
2204
+ return "\t";
2205
+ case "v":
2206
+ return "\v";
2207
+ case "x":
2208
+ return String.fromCharCode(parseInt(match.slice(2), 16));
2209
+ case "u":
2210
+ if (match[2] === "{") {
2211
+ return String.fromCodePoint(parseInt(match.slice(3, -1), 16));
2212
+ }
2213
+ return String.fromCharCode(parseInt(match.slice(2), 16));
2214
+ case "0":
2215
+ return "\0";
2216
+ default:
2217
+ return match.slice(1);
2218
+ }
2219
+ });
2220
+ }
2221
+ props[prop.name] = value;
2222
+ }
2223
+ else {
2224
+ // spread prop
2225
+ props = { ...props, ...prop.value };
2226
+ }
2227
+ }
2228
+ return createElement(parsed.open.value, props, ...children);
2229
+ }
2230
+ function formatTagForError(tag) {
2231
+ return typeof tag === "function"
2232
+ ? tag.name + "()"
2233
+ : typeof tag === "string"
2234
+ ? `"${tag}"`
2235
+ : JSON.stringify(tag);
2236
+ }
2237
+
1815
2238
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
1816
2239
  const impl$1 = {
1817
2240
  parse(text) {
@@ -2192,6 +2615,7 @@
2192
2615
  exports.dom = dom;
2193
2616
  exports.html = html;
2194
2617
  exports.isElement = isElement;
2618
+ exports.jsx = jsx;
2195
2619
 
2196
2620
  Object.defineProperty(exports, '__esModule', { value: true });
2197
2621