A deliberately narrow ASCII line-width checker in JavaScript
Define the contract before measuring
A character-art preview often needs a modest answer before it needs another visual effect: does this sample satisfy the width rule we actually chose? This article develops a deliberately narrow JavaScript checker for printable ASCII rows. It is not a universal text-layout engine, an accessibility audit, or a claim about every publishing surface. I work on ASCII Art Generator, and this entire article was drafted with AI assistance. The project provides free core browser tools and optional AI features that can require paid credits. The example checker below is an independent educational exercise, not a description of the project's internal implementation or a promise that it is built into the service.
The contract comes first. For this exercise, a valid row consists only of printable characters in the seven-bit ASCII range, including ordinary spaces. A tab makes the row unsupported, and other control characters or non-ASCII characters make it unsupported as well. The checker reports that uncertainty instead of guessing a visual width. Line endings are normalized to line feeds before rows are counted. A final line break deliberately produces a final empty row. An empty string represents one empty row. These are design decisions for the sample, not discoveries about how every editor, terminal, or browser treats its input.
Why choose such a restricted contract? A JavaScript string's length is not a general measurement of displayed columns. Once combining marks, emoji, or other Unicode characters enter the picture, several different concepts can be relevant: code units, code points, grapheme clusters, font shaping, and the destination's own layout rules. Even knowing a character's category does not establish the result under every font and container. Restricting the input does not solve those larger problems. It makes the smaller result explainable. A reader can see exactly when the checker offers a number and when it refuses to present a number as though it were established.
The width limit is another part of the contract, not a property inferred from a device name. The function accepts a positive safe integer and uses eighty only as a convenient default for the demonstration. A caller should replace that value with the constraint of the intended destination when it is known. If the destination has not been selected, label the limit provisional. Do not describe a sample as mobile safe merely because it fits within an arbitrary number. A narrow panel can wrap unexpectedly, while a wider panel can still use a proportional font. This checker does not inspect either condition.
There is also a sample-size bound. The function refuses a source longer than twenty thousand UTF-16 code units before building its row report. That bound is an implementation choice intended to keep the educational example small, not a security guarantee or a measured throughput limit. A production application would need its own input policy and error handling. The example does not fetch data, write files, or transmit the source text. Its return value describes row numbers, supported column counts, and reasons for refusal without returning copies of the original rows. That can reduce accidental exposure in a diagnostic display, but it does not certify the privacy of an application surrounding the function.
A deliberately restricted implementation
export function inspectAsciiGrid(source, limit = 80) {
if (typeof source !== 'string') throw new TypeError('Expected text');
if (source.length > 20000) throw new RangeError('Sample exceeds 20000 UTF-16 units');
if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError('Expected a positive integer limit');
const lines = source.replace(/\r\n?/g, '\n').split('\n');
const rows = lines.map((line, index) => {
const reason = line.includes('\t') ? 'tab'
: /[^\x20-\x7e]/.test(line) ? 'non-printable-or-non-ascii' : null;
const columns = reason ? null : line.length;
return { row: index + 1, columns, overLimit: reason ? null : columns > limit, reason };
});
const valid = rows.every(row => row.reason === null);
return {
rowCount: rows.length,
valid,
maxColumns: valid ? rows.reduce((n, row) => Math.max(n, row.columns), 0) : null,
rows
};
}
The first branch validates the caller's arguments. Rejecting a non-string input avoids silently converting an object or a missing value into a representation that the user did not intend to inspect. Rejecting a non-positive or unsafe limit avoids presenting an invalid policy as an ordinary result. The source-size check is separate from the width check: a short source can contain an over-limit row, and a long collection of short rows can exceed the sample bound. Those cases deserve different explanations. Keeping them separate also makes a small test suite more informative than one broad example that happens to pass.
The row mapping uses a simple priority. If a row contains a tab, its reason is tab. Otherwise, a character outside the printable range produces the broader unsupported-character reason. The function does not claim to list every issue in a row. This priority is sufficient for explaining why a column count is withheld, but a caller that needs a complete validation report should design a different result shape. Naming that limitation matters. An application should not convert one selected reason into a claim that all remaining characters were checked against some richer, undocumented layout policy.
For supported rows, ordinary string length is useful because the accepted range has already removed the cases this exercise declines to interpret. The report compares that count with the supplied limit. It does not trim leading or trailing spaces, collapse repeated spaces, normalize punctuation, or repair the artifact. Such transformations could change a character picture or a deliberate separator. If an application wants to propose a repair, that proposal should be a separate operation with a visible before-and-after comparison. Validation becomes difficult to trust when it silently edits the material that it later announces as valid.
The aggregate maximum is withheld if any row is unsupported. Reporting the maximum of only the supported subset could look like a complete answer even though an unknown row might determine the real presentation constraint. The individual row results remain available, so a caller can explain where the uncertainty arose. This is a small example of designing a result type around what is actually known. It is often preferable to a single boolean accompanied by a success-colored badge. A boolean can be useful, but the interface should not discard the distinction between an over-limit supported row and a row whose displayed width was never evaluated.
Evidence and interface boundaries
I ran the accompanying deterministic tests locally under Node. Ten string cases passed: an empty source, a small three-row box, CRLF input, a trailing line break, a tab, an accented character, an escape control, an over-limit row, an invalid limit, and an oversized source. These tests check the function's stated string contract. They do not measure browser rendering, prove compatibility with a terminal, exercise the public generator, or validate any server deployment. The distinction is intentional. A passing unit test for this function should not be promoted into an unsupported claim about a much larger product or a future publishing destination.
A user interface can present the report without pretending to know more than the function knows. Show the selected limit, the row count, and the refusal reason when one exists. If all rows are supported but one is over the limit, say that the sample exceeds this chosen rule. Do not say that the artwork is broken everywhere. If a row contains an unsupported character, ask the user to choose a different inspection method or explicitly revise the contract. Avoid converting the text behind the scenes just to obtain a green result. An honest refusal is more useful than a reassuring number whose meaning has changed without notice.
The browser project ascii art generator is a relevant place to create character-art candidates, but a candidate and an accepted destination rendering are different artifacts. Keep a plain-text source sample alongside any permitted screenshot used for comparison. Inspect the saved destination through its normal editing and viewing path when that step is available. A composer preview can differ from a saved page, and a screenshot does not preserve an editable sequence. The checker can support one preliminary question about a restricted sample. It cannot replace those destination checks or establish how a future collaborator's application will handle the content.
Keep meaningful words available outside the decorative arrangement. If a grid carries a project name or an instruction, provide that information in ordinary prose as well. This suggestion is not an accessibility certification. It avoids making one fragile visual representation the only carrier of essential meaning. Similarly, use source material you are permitted to share when demonstrating image-to-character workflows. A converter's acceptance of an upload does not determine ownership or permission to publish it. A public example should clearly distinguish an original test sample from a private input that has not been disclosed.
The useful outcome is a small, inspectable boundary: these printable ASCII rows were counted under this limit, these other rows were not assigned a column count, and these local tests cover only that contract. A later implementation can support tabs, Unicode, or a particular rendering environment, but it should state the additional assumptions and provide appropriate evidence. Extending a function is not the same as extending its proof. Keeping that distinction visible helps a character-art workflow remain understandable when a sample moves from an editor to a document, a portfolio, or a different person's publishing system.

Official project illustration, included for context; not a test result or a screenshot of the checker.
All rights reserved