feat(ui): add DataTable and Toaster, drop the legacy Table, fix overlay dialogs
DataTable replaces the 20-line Table scaffold entirely: columns, sorting, filtering, pagination, selection, bulk actions, comparison layout, sticky first column, custom HTML cells, and a remote source driven by a `request` output rather than a function prop (props travel as HTML attributes, so a function arrives as its own source text). Toaster replaces the hand-rolled status div: tone icons, actions, hover pause/resume and a progress bar. Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing ever moved focus into the panel, so the @keydown handler on their root never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap and a body scroll lock now live in the reactive runtime, shared by both. ContextMenu placed pointer menus by subtracting a guessed 340x420 from the viewport, which pushed every menu that was not that size away from the pointer; it now positions at the pointer and lets the anchored clamp pull it back once it can be measured. The reactive runtime size budget moves 150k -> 175k to cover anchored overlays, dialog behaviour, the toaster and the DataTable client half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||
// WRN editor compiler source hash: 1d2e0d1e1c38a4513b0fce3631bae003f7c28069920c5386ecc68cbe728bf57b
|
||||
// WRN editor compiler source hash: 451706f47a7734fd5808cdeeb5786e1b97a183afec98db99fc041369eb16a205
|
||||
// WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
|
||||
// Generated with TypeScript: 5.9.3
|
||||
const __nodeRequire = require;
|
||||
@@ -560,24 +560,44 @@ function browserModuleRequired(ast) {
|
||||
}
|
||||
function functionEntry(ast, fn, availableFunctions) {
|
||||
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
||||
/*
|
||||
* Names the function body declares for itself.
|
||||
*
|
||||
* Props and state are destructured into the SAME scope as the body, so a
|
||||
* body that declares `var size` when `size` is also a prop produced
|
||||
* "Identifier 'size' has already been declared" and the entire module
|
||||
* failed to parse -- taking every function in the component down with it,
|
||||
* with nothing to point at the one line responsible. Skipping the alias for
|
||||
* a shadowed name is also what plain JavaScript does: inside that function
|
||||
* the local wins.
|
||||
*/
|
||||
const declaredLocals = new Set();
|
||||
for (const match of fn.body.matchAll(/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)|\bfunction\s+([A-Za-z_$][\w$]*)/g)) {
|
||||
const name = match[1] ?? match[2];
|
||||
if (name)
|
||||
declaredLocals.add(name);
|
||||
}
|
||||
const stateNames = ast.states
|
||||
.filter((state) => state.runtime !== "server" &&
|
||||
safeIdentifier(state.name) &&
|
||||
!RUNTIME_BINDINGS.has(state.name) &&
|
||||
!parameterNames.has(state.name))
|
||||
!parameterNames.has(state.name) &&
|
||||
!declaredLocals.has(state.name))
|
||||
.map((state) => state.name);
|
||||
const stateSet = new Set(stateNames);
|
||||
const propNames = ast.props
|
||||
.filter((prop) => safeIdentifier(prop.name) &&
|
||||
!RUNTIME_BINDINGS.has(prop.name) &&
|
||||
!parameterNames.has(prop.name) &&
|
||||
!stateSet.has(prop.name))
|
||||
!stateSet.has(prop.name) &&
|
||||
!declaredLocals.has(prop.name))
|
||||
.map((prop) => prop.name);
|
||||
const functionAliases = availableFunctions.filter((name) => safeIdentifier(name) &&
|
||||
!RUNTIME_BINDINGS.has(name) &&
|
||||
!parameterNames.has(name) &&
|
||||
!stateSet.has(name) &&
|
||||
!propNames.includes(name));
|
||||
!propNames.includes(name) &&
|
||||
!declaredLocals.has(name));
|
||||
const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
|
||||
const initialStateSnapshot = stateNames.length
|
||||
? `const __wrnexusInitialState = { ${stateNames.map((name) => `${JSON.stringify(name)}: context.state.${name}`).join(", ")} };`
|
||||
@@ -805,6 +825,31 @@ function eventAttribute(name) {
|
||||
}
|
||||
return `data-on-${name}`;
|
||||
}
|
||||
/**
|
||||
* Event attribute for a handler written on a *component tag*
|
||||
* (`<Modal @confirm="save()">`).
|
||||
*
|
||||
* These need their own attribute name. The mount's attributes are forwarded
|
||||
* into the component and land on its view root, i.e. inside the component's
|
||||
* own `data-scope` -- but the statement (`save()`) belongs to the parent that
|
||||
* wrote the tag. Emitting `data-on-confirm` makes the child's runtime bind it
|
||||
* against the child's scope, where the parent's functions and state do not
|
||||
* exist, so the handler silently does nothing. `data-wrn-out-*` is ignored by
|
||||
* the child and claimed by the mounting scope instead.
|
||||
*
|
||||
* `window:`/`document:` (and the browser/mobile bridges) keep the plain
|
||||
* `data-on-*` form: those bind to a global target rather than to the element,
|
||||
* and the runtime has no component-output path for them.
|
||||
*/
|
||||
function componentEventAttribute(name) {
|
||||
if (name.startsWith("window:") ||
|
||||
name.startsWith("document:") ||
|
||||
name.startsWith("browser-") ||
|
||||
name.startsWith("mobile-")) {
|
||||
return eventAttribute(name);
|
||||
}
|
||||
return `data-wrn-out-${name}`;
|
||||
}
|
||||
function reactiveAttrValue(raw, reactive) {
|
||||
let found = false;
|
||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
||||
@@ -962,7 +1007,11 @@ function renderLoopBody(node) {
|
||||
const attrs = node.attrs
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
||||
const name = attr.event
|
||||
? componentTag
|
||||
? componentEventAttribute(attr.name)
|
||||
: eventAttribute(attr.name)
|
||||
: attr.name;
|
||||
if (attr.boolean) {
|
||||
return escLit(` ${name}`);
|
||||
}
|
||||
@@ -1221,7 +1270,9 @@ function renderNestedComponentInvocation(node, ctx) {
|
||||
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1])})}`;
|
||||
}
|
||||
if (attr.event) {
|
||||
return (escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`));
|
||||
return (escLit(` ${componentEventAttribute(attr.name)}="`) +
|
||||
escLit(attrEscape(attr.value)) +
|
||||
escLit(`"`));
|
||||
}
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
@@ -2398,6 +2449,18 @@ function renderComponentNode(node, ctx) {
|
||||
const marker = referencesState || referencesLoopVariable || referencesServerLocal
|
||||
? ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"`
|
||||
: "";
|
||||
/*
|
||||
* A boolean attribute whose expression names a loop variable cannot
|
||||
* be resolved on the server: __wireBooleanAttr runs at render time,
|
||||
* where `row` or `item` simply does not exist, and the emitted
|
||||
* module blew up. Leave the attribute off the server output and let
|
||||
* the client bind set it -- the runtime toggles boolean attributes
|
||||
* rather than stringifying them, so `checked={isSelected(row)}`
|
||||
* behaves correctly once hydrated.
|
||||
*/
|
||||
if (referencesLoopVariable) {
|
||||
return ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"`;
|
||||
}
|
||||
return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
|
||||
}
|
||||
if (a.value === "false")
|
||||
@@ -2406,6 +2469,20 @@ function renderComponentNode(node, ctx) {
|
||||
return ` ${a.name}`;
|
||||
}
|
||||
const wholeExpression = wholeAttributeExpression(a.value);
|
||||
/*
|
||||
* data-show carries an EXPRESSION, not a value. The client re-evaluates
|
||||
* whatever string it finds in the attribute on every state change, so
|
||||
* interpolating `{open || visible}` down to the literal "false" at
|
||||
* render time froze the directive: the element could never be shown
|
||||
* again, no matter what the state did. A data-wrn-bind marker did not
|
||||
* save it either -- the bind rewrites the same attribute the directive
|
||||
* reads, and the directive had already captured "false" as its
|
||||
* expression. Emitting the expression verbatim (the form Modal uses,
|
||||
* data-show="isOpen()") makes both authoring styles behave the same.
|
||||
*/
|
||||
if (a.name === "data-show" && wholeExpression) {
|
||||
return ` data-show="${escLit(attrEscape(wholeExpression))}"`;
|
||||
}
|
||||
const compiledValue = isExplicitComponentMount && wholeExpression
|
||||
? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||
: compileAttrValue(a.value, elementContext);
|
||||
@@ -2751,7 +2828,10 @@ function __wireSpreadAttrs(value: any): string {
|
||||
lowerName === "style" ||
|
||||
lowerName === "slot" ||
|
||||
lowerName === "data-component" ||
|
||||
lowerName.startsWith("data-wrn")
|
||||
// Internal markers must not leak through a spread -- except the
|
||||
// parent's output handlers, whose whole job is to ride from the mount
|
||||
// onto the view root so the mounting scope can bind them there.
|
||||
(lowerName.startsWith("data-wrn") && !lowerName.startsWith("data-wrn-out-"))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -2867,7 +2947,7 @@ function __wireProp(v) {
|
||||
}
|
||||
function renderPageComponentAttr(attr, dynamicExpressions) {
|
||||
if (attr.event) {
|
||||
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
@@ -6703,6 +6783,18 @@ class Lexer {
|
||||
* Read a `{ ... }` block and return its INNER text (no outer braces), with
|
||||
* brace counting that respects string and template literals so a `}` inside a
|
||||
* string doesn't end the block early.
|
||||
*
|
||||
* Comments are skipped as well. Without that, an apostrophe in ordinary
|
||||
* prose — `/* the panel's color *\/`, `// the Input's slot` — opened a
|
||||
* string that ran to the next apostrophe, swallowing every brace in between
|
||||
* and failing the whole component with "Unbalanced braces" pointing at the
|
||||
* block's opening line. Comments are where apostrophes actually occur, so
|
||||
* that error was almost always a false alarm.
|
||||
*
|
||||
* A `//` line comment is only recognised at the start of a line (after
|
||||
* whitespace), which is where every comment in a `.wrn` file is written.
|
||||
* Recognising it mid-line would break the far more common case of a bare
|
||||
* URL in view text, where `https://…` is not inside quotes.
|
||||
*/
|
||||
readBalancedBraces() {
|
||||
this.skipTrivia();
|
||||
@@ -6714,6 +6806,8 @@ class Lexer {
|
||||
let depth = 0;
|
||||
let i = this.pos;
|
||||
let str = null;
|
||||
/** True while only whitespace has been seen since the last newline. */
|
||||
let atLineStart = false;
|
||||
for (; i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (str) {
|
||||
@@ -6725,6 +6819,27 @@ class Lexer {
|
||||
str = null;
|
||||
continue;
|
||||
}
|
||||
if (c === "\n") {
|
||||
atLineStart = true;
|
||||
continue;
|
||||
}
|
||||
if (c === "/" && src[i + 1] === "*") {
|
||||
const close = src.indexOf("*/", i + 2);
|
||||
if (close === -1)
|
||||
break; // unterminated: fall through to the error
|
||||
i = close + 1;
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
if (atLineStart && c === "/" && src[i + 1] === "/") {
|
||||
const newline = src.indexOf("\n", i + 2);
|
||||
if (newline === -1)
|
||||
break;
|
||||
i = newline - 1; // let the loop's own increment land on the newline
|
||||
continue;
|
||||
}
|
||||
if (c !== " " && c !== "\t" && c !== "\r")
|
||||
atLineStart = false;
|
||||
if (c === '"' || c === "'" || c === "`") {
|
||||
str = c;
|
||||
continue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// WRN editor extension source hash: 825c406dea4b196976b1b6e53b3c71c3263bd63f403acdaf2fe7e5ad174275da
|
||||
// WRN editor extension source hash: 174fee91d6ec09c86f71f1204aeb46a20e5a563cc8af1484051d68dde01c8e10
|
||||
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
||||
"use strict";
|
||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||
@@ -23204,10 +23204,16 @@ ${(0, codegen_ts_1.generate)(ast)}`,
|
||||
}
|
||||
function functionEntry(ast, fn, availableFunctions) {
|
||||
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
||||
const stateNames = ast.states.filter((state) => state.runtime !== "server" && safeIdentifier(state.name) && !RUNTIME_BINDINGS.has(state.name) && !parameterNames.has(state.name)).map((state) => state.name);
|
||||
const declaredLocals = new Set;
|
||||
for (const match of fn.body.matchAll(/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)|\bfunction\s+([A-Za-z_$][\w$]*)/g)) {
|
||||
const name = match[1] ?? match[2];
|
||||
if (name)
|
||||
declaredLocals.add(name);
|
||||
}
|
||||
const stateNames = ast.states.filter((state) => state.runtime !== "server" && safeIdentifier(state.name) && !RUNTIME_BINDINGS.has(state.name) && !parameterNames.has(state.name) && !declaredLocals.has(state.name)).map((state) => state.name);
|
||||
const stateSet = new Set(stateNames);
|
||||
const propNames = ast.props.filter((prop) => safeIdentifier(prop.name) && !RUNTIME_BINDINGS.has(prop.name) && !parameterNames.has(prop.name) && !stateSet.has(prop.name)).map((prop) => prop.name);
|
||||
const functionAliases = availableFunctions.filter((name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !parameterNames.has(name) && !stateSet.has(name) && !propNames.includes(name));
|
||||
const propNames = ast.props.filter((prop) => safeIdentifier(prop.name) && !RUNTIME_BINDINGS.has(prop.name) && !parameterNames.has(prop.name) && !stateSet.has(prop.name) && !declaredLocals.has(prop.name)).map((prop) => prop.name);
|
||||
const functionAliases = availableFunctions.filter((name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !parameterNames.has(name) && !stateSet.has(name) && !propNames.includes(name) && !declaredLocals.has(name));
|
||||
const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
|
||||
const initialStateSnapshot = stateNames.length ? `const __wrnexusInitialState = { ${stateNames.map((name) => `${JSON.stringify(name)}: context.state.${name}`).join(", ")} };` : "";
|
||||
const stateAliases = stateNames.length ? `let { ${stateNames.join(", ")} } = context.state;` : "";
|
||||
@@ -23404,6 +23410,12 @@ export function bindClientScope(context) {
|
||||
}
|
||||
return `data-on-${name}`;
|
||||
}
|
||||
function componentEventAttribute(name) {
|
||||
if (name.startsWith("window:") || name.startsWith("document:") || name.startsWith("browser-") || name.startsWith("mobile-")) {
|
||||
return eventAttribute(name);
|
||||
}
|
||||
return `data-wrn-out-${name}`;
|
||||
}
|
||||
function reactiveAttrValue(raw, reactive) {
|
||||
let found = false;
|
||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
||||
@@ -23527,7 +23539,7 @@ export function bindClientScope(context) {
|
||||
}
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
||||
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
||||
const name = attr.event ? componentTag ? componentEventAttribute(attr.name) : eventAttribute(attr.name) : attr.name;
|
||||
if (attr.boolean) {
|
||||
return escLit(` ${name}`);
|
||||
}
|
||||
@@ -23699,7 +23711,7 @@ export function bindClientScope(context) {
|
||||
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1])})}`;
|
||||
}
|
||||
if (attr.event) {
|
||||
return escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`);
|
||||
return escLit(` ${componentEventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`);
|
||||
}
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
@@ -24751,6 +24763,9 @@ ${handlers.join(`
|
||||
const referencesLoopVariable2 = elementContext.loopVars ? exprRefsState(a.value, elementContext.loopVars) : false;
|
||||
const referencesServerLocal2 = ctx.serverLocals ? exprRefsState(a.value, ctx.serverLocals) : false;
|
||||
const marker2 = referencesState2 || referencesLoopVariable2 || referencesServerLocal2 ? ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"` : "";
|
||||
if (referencesLoopVariable2) {
|
||||
return ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"`;
|
||||
}
|
||||
return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker2}`;
|
||||
}
|
||||
if (a.value === "false")
|
||||
@@ -24759,6 +24774,9 @@ ${handlers.join(`
|
||||
return ` ${a.name}`;
|
||||
}
|
||||
const wholeExpression = wholeAttributeExpression(a.value);
|
||||
if (a.name === "data-show" && wholeExpression) {
|
||||
return ` data-show="${escLit(attrEscape(wholeExpression))}"`;
|
||||
}
|
||||
const compiledValue = isExplicitComponentMount && wholeExpression ? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}` : compileAttrValue(a.value, elementContext);
|
||||
const rendered = ` ${a.name}="${compiledValue}"`;
|
||||
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
||||
@@ -25054,7 +25072,10 @@ function __wireSpreadAttrs(value: any): string {
|
||||
lowerName === "style" ||
|
||||
lowerName === "slot" ||
|
||||
lowerName === "data-component" ||
|
||||
lowerName.startsWith("data-wrn")
|
||||
// Internal markers must not leak through a spread -- except the
|
||||
// parent's output handlers, whose whole job is to ride from the mount
|
||||
// onto the view root so the mounting scope can bind them there.
|
||||
(lowerName.startsWith("data-wrn") && !lowerName.startsWith("data-wrn-out-"))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -25173,7 +25194,7 @@ function __wireRaw(v: any): string {
|
||||
}
|
||||
function renderPageComponentAttr(attr, dynamicExpressions) {
|
||||
if (attr.event) {
|
||||
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
@@ -28961,6 +28982,7 @@ ${serverFunctions}
|
||||
let depth = 0;
|
||||
let i = this.pos;
|
||||
let str = null;
|
||||
let atLineStart = false;
|
||||
for (;i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (str) {
|
||||
@@ -28972,6 +28994,29 @@ ${serverFunctions}
|
||||
str = null;
|
||||
continue;
|
||||
}
|
||||
if (c === `
|
||||
`) {
|
||||
atLineStart = true;
|
||||
continue;
|
||||
}
|
||||
if (c === "/" && src[i + 1] === "*") {
|
||||
const close = src.indexOf("*/", i + 2);
|
||||
if (close === -1)
|
||||
break;
|
||||
i = close + 1;
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
if (atLineStart && c === "/" && src[i + 1] === "/") {
|
||||
const newline = src.indexOf(`
|
||||
`, i + 2);
|
||||
if (newline === -1)
|
||||
break;
|
||||
i = newline - 1;
|
||||
continue;
|
||||
}
|
||||
if (c !== " " && c !== "\t" && c !== "\r")
|
||||
atLineStart = false;
|
||||
if (c === '"' || c === "'" || c === "`") {
|
||||
str = c;
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// WRN editor language server source hash: 5397c7912894dd5ab330f6fafc0f4093adbb94d95c23366ba7883605a724a7ae
|
||||
// WRN editor language server source hash: de46b5767d808fb99bb470ac9ccd0bc9504aa9ab40f0cf8ad9ec4e945303200d
|
||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||
// @bun @bun-cjs
|
||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||
@@ -169020,6 +169020,7 @@ class Lexer {
|
||||
let depth = 0;
|
||||
let i = this.pos;
|
||||
let str = null;
|
||||
let atLineStart = false;
|
||||
for (;i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (str) {
|
||||
@@ -169031,6 +169032,29 @@ class Lexer {
|
||||
str = null;
|
||||
continue;
|
||||
}
|
||||
if (c === `
|
||||
`) {
|
||||
atLineStart = true;
|
||||
continue;
|
||||
}
|
||||
if (c === "/" && src[i + 1] === "*") {
|
||||
const close = src.indexOf("*/", i + 2);
|
||||
if (close === -1)
|
||||
break;
|
||||
i = close + 1;
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
if (atLineStart && c === "/" && src[i + 1] === "/") {
|
||||
const newline = src.indexOf(`
|
||||
`, i + 2);
|
||||
if (newline === -1)
|
||||
break;
|
||||
i = newline - 1;
|
||||
continue;
|
||||
}
|
||||
if (c !== " " && c !== "\t" && c !== "\r")
|
||||
atLineStart = false;
|
||||
if (c === '"' || c === "'" || c === "`") {
|
||||
str = c;
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user