<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: OnePassApps</title>
    <description>The latest articles on DEV Community by OnePassApps (@onepassapps).</description>
    <link>https://dev.to/onepassapps</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4222432%2F971441a4-25b9-4a1f-9985-337cdf95d706.png</url>
      <title>DEV Community: OnePassApps</title>
      <link>https://dev.to/onepassapps</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/onepassapps"/>
    <language>en</language>
    <item>
      <title>Validate JSON in the Browser: The Line and Column of the Mistake</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 06:20:19 +0000</pubDate>
      <link>https://dev.to/onepassapps/validate-json-in-the-browser-the-line-and-column-of-the-mistake-3m30</link>
      <guid>https://dev.to/onepassapps/validate-json-in-the-browser-the-line-and-column-of-the-mistake-3m30</guid>
      <description>&lt;p&gt;I work on OnePassApps, and this is what our Data Studio does when it hands you a JSON syntax error: it tells you the line, the column and the byte offset, and it puts a caret under the exact character where the parser gave up.&lt;/p&gt;

&lt;p&gt;That last part is the interesting one. Most validators say "invalid JSON" and leave you to hunt. This one names the character it stopped on, and that character is usually &lt;em&gt;not&lt;/em&gt; where your mistake is.&lt;/p&gt;

&lt;p&gt;You can try it on the &lt;a href="https://app.onepassapps.com/studios/data?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=json-validator&amp;amp;utm_content=data-studio-link" rel="noopener noreferrer"&gt;Data Studio&lt;/a&gt; page, or read the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=json-validator&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps guides page&lt;/a&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel, before you run it
&lt;/h2&gt;

&lt;p&gt;The Data Studio has eleven tabs: Viewer, Sort / filter, Columns, Duplicates, Merge, Split, To JSON, Formatter, Validator, To CSV and YAML. The Validator tab is a plain three-step panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01 Source&lt;/strong&gt; - Clear, Paste local data, Browse files, Input limits and privacy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02 Options&lt;/strong&gt; - one sentence, and it is the whole contract: &lt;code&gt;Checks syntax only; the structure is not compared with a schema.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03 Result&lt;/strong&gt; - empty until you run it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The action button reads &lt;code&gt;Validate JSON&lt;/code&gt;, while the tab radio above it reads &lt;code&gt;Validator&lt;/code&gt;. That radio/button collision is the same trap in every studio, so match the full button text.&lt;/p&gt;

&lt;h2&gt;
  
  
  A run on a deliberately broken file
&lt;/h2&gt;

&lt;p&gt;I pasted a four-line object with one missing bracket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OnePassApps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"credits"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The array on line 3 is never closed. The result panel reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status &lt;code&gt;Not valid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Line 4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Column 12&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a timing line - 41 ms in this run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stage below it is headed &lt;code&gt;JSON VALIDATOR&lt;/code&gt; and &lt;code&gt;Not valid JSON&lt;/code&gt;, and it repeats the whole message:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Line 4, column 12: Expected ',' or ']' after array element in JSON at position 59 (line 4 column 12)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;and prints the source back with line numbers and a caret under the character it stopped at.&lt;/p&gt;

&lt;p&gt;Line 4 is &lt;code&gt;"credits": 100000&lt;/code&gt;, and column 12 is the colon after "credits". The parser was still inside the open array, so it reached that colon and wanted either a comma or the closing bracket. &lt;strong&gt;The missing bracket is on line 3.&lt;/strong&gt; The tool is not pointing at your typo - it is pointing at where it stopped parsing, which is standard JavaScript parser behaviour, and this tool does not paper over it.&lt;/p&gt;

&lt;p&gt;I checked the message independently: &lt;code&gt;JSON.parse&lt;/code&gt; in Node throws &lt;code&gt;Expected ',' or ']' after array element in JSON at position 59 (line 4 column 12)&lt;/code&gt; for the same bytes. Position 59 is the same character, the colon, and the offset, the line and the column all agree.&lt;/p&gt;

&lt;p&gt;Second case, a trailing comma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;code&gt;Line 4, column 1: Expected double-quoted property name in JSON at position 22 (line 4 column 1)&lt;/code&gt;. Again line 4 is where it stopped; the trailing comma is on line 3. Position 22 is the closing brace.&lt;/p&gt;

&lt;h2&gt;
  
  
  A valid file, and what the counters actually count
&lt;/h2&gt;

&lt;p&gt;Now a valid one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OnePassApps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"credits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audio"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The panel reads &lt;code&gt;Valid&lt;/code&gt;, then &lt;code&gt;Valid JSON.&lt;/code&gt;, then &lt;code&gt;Format&lt;/code&gt;, &lt;code&gt;JSON&lt;/code&gt;, &lt;code&gt;Size 90 B&lt;/code&gt;, &lt;code&gt;Lines 8&lt;/code&gt;, and a timing line - 53 ms in that run. The stage says &lt;code&gt;Valid JSON&lt;/code&gt; and &lt;code&gt;8 lines · 90 B · from Pasted JSON&lt;/code&gt;, and adds one line worth reading: &lt;code&gt;The parsed structure, formatted with two-space indentation.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here is the subtlety. The pasted text was 78 bytes over 5 lines. The counters said 90 B and 8 lines. Those numbers describe the &lt;strong&gt;formatted output&lt;/strong&gt;, not what you pasted - the formatter expands the &lt;code&gt;"tags"&lt;/code&gt; array onto four lines before it counts anything. I re-ran the same JSON through a standard two-space dump and got the same 90 bytes and the same 8 lines, character for character.&lt;/p&gt;

&lt;p&gt;So the validator's size and line counts are a preview of the formatted JSON. If you want to know how big your paste was, measure the paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Formatter tab, and the 28 bytes that appear
&lt;/h2&gt;

&lt;p&gt;The Formatter tab sits next to it with the same three-step panel, and its only option is the indent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;02 Options&lt;/strong&gt; - &lt;code&gt;Indent&lt;/code&gt;, then &lt;code&gt;2 spaces&lt;/code&gt; or &lt;code&gt;4 spaces&lt;/code&gt;, and the note &lt;code&gt;Keys keep their order. Changing the indent clears the result.&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both halves of that sentence are true. Switching from 2 spaces to 4 spaces wipes the result - the output box empties and the panel falls back to &lt;code&gt;Ready when you are&lt;/code&gt; and &lt;code&gt;Format JSON to see the result here.&lt;/code&gt; - while your pasted source stays put, so you press &lt;code&gt;Format JSON&lt;/code&gt; again and it is back.&lt;/p&gt;

&lt;p&gt;Two runs on the same six-line paste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 spaces: &lt;code&gt;12 lines · 147 B&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;4 spaces: &lt;code&gt;12 lines · 175 B&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same line count, 28 bytes more, because every indent level costs one extra space. I dumped the same object with a standard JSON formatter at indent 2 and at indent 4 and got exactly 147 and exactly 175 bytes, byte for byte. This is an ordinary two-space or four-space JSON dump, not a house style with opinions.&lt;/p&gt;

&lt;p&gt;The formatter also expands arrays. &lt;code&gt;"studios": ["files", "audio"]&lt;/code&gt; comes out as four lines, one item per line, and the nested object gets its own indented block.&lt;/p&gt;

&lt;p&gt;After a run the action button becomes &lt;code&gt;Format again&lt;/code&gt;, and the stage reads &lt;code&gt;GENERATED OUTPUT&lt;/code&gt;, &lt;code&gt;Generated JSON&lt;/code&gt;, &lt;code&gt;12 lines · 147 B · from Pasted JSON&lt;/code&gt;, with a &lt;code&gt;Download JSON&lt;/code&gt; button.&lt;/p&gt;

&lt;h2&gt;
  
  
  The downloaded file is the same bytes
&lt;/h2&gt;

&lt;p&gt;I clicked &lt;code&gt;Download JSON&lt;/code&gt; and read the file off disk: &lt;code&gt;data.json&lt;/code&gt;, 147 bytes, byte-identical to the two-space output shown on screen. The download is the formatted result, not your original paste - which is the useful behaviour if you were about to save a file you had just edited into a JSON shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and the privacy line
&lt;/h2&gt;

&lt;p&gt;Opening &lt;code&gt;Input limits and privacy&lt;/code&gt; on this tab says: pasted text supports up to 10,000 lines; pasted CSV up to 8 MiB and 10,000 data rows; CSV files up to 20 MiB, 100,000 rows and 2 million cells; JSON up to 2 MiB and YAML up to 1 MiB. It ends with the sentence "Your data stays on this device."&lt;/p&gt;

&lt;p&gt;The Formatter tab also carries the line &lt;code&gt;Nothing leaves this tab.&lt;/code&gt; under its 03 Result heading, and "Processing stays in this browser." is the line below the result stage - accurate here, because the parse, the format, the byte count and the download all happen in the tab. The plan block on the same page reads "Standard is free right now", "$0 / month", "100,000 credits every 30 days", "No end date set" and "No card required".&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it
&lt;/h2&gt;

&lt;p&gt;Use the Validator when you need the exact offset of a syntax error - in a CI log, in a bug report, in a message pasted from another tool. Use the Formatter when you want a canonical two-space or four-space dump of JSON you already know parses.&lt;/p&gt;

&lt;p&gt;Neither of them is a schema validator, and the panel says so in as many words: it will tell you that your JSON parses, and it will not tell you that a field you expected is missing. There is no schema box on this tab, because this tab checks syntax.&lt;/p&gt;

&lt;p&gt;I work on OnePassApps, the team behind app.onepassapps.com. The Data Studio also does CSV viewing, sorting and filtering, de-duplicating rows, merging, splitting, CSV-to-JSON, JSON-to-CSV and YAML, if you want the same trust in those.&lt;/p&gt;

</description>
      <category>json</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>Convert CSV to JSON in the Browser: Every Value Stays a String</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:34:35 +0000</pubDate>
      <link>https://dev.to/onepassapps/convert-csv-to-json-in-the-browser-every-value-stays-a-string-57n2</link>
      <guid>https://dev.to/onepassapps/convert-csv-to-json-in-the-browser-every-value-stays-a-string-57n2</guid>
      <description>&lt;p&gt;I work on OnePassApps, and this is what our Data Studio does when it turns a CSV into JSON: it hands back exactly what the CSV said, and nothing more.&lt;/p&gt;

&lt;p&gt;Every value comes out as a JSON &lt;strong&gt;string&lt;/strong&gt;. The number &lt;code&gt;42.5&lt;/code&gt; comes out as the string &lt;code&gt;"42.5"&lt;/code&gt;. The word &lt;code&gt;true&lt;/code&gt; comes out as the string &lt;code&gt;"true"&lt;/code&gt;. An empty cell comes out as an empty string, not &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a conservative choice, and for a converter it is usually the right one, but it is also the thing that catches people out. Here is exactly what happens, and how I checked it.&lt;/p&gt;

&lt;p&gt;You can try it on the &lt;a href="https://app.onepassapps.com/studios/data?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=csv-to-json&amp;amp;utm_content=data-studio-link" rel="noopener noreferrer"&gt;Data Studio&lt;/a&gt; page, or read the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=csv-to-json&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps guides page&lt;/a&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel, before you run it
&lt;/h2&gt;

&lt;p&gt;Open the Data Studio, switch to the &lt;strong&gt;To JSON&lt;/strong&gt; tab, paste or drop a CSV, and press the action button. The button reads &lt;code&gt;Convert CSV to JSON&lt;/code&gt; - not "To JSON", which is the tab radio. That label mismatch is the same trap you hit in the other studios, so match the full button text.&lt;/p&gt;

&lt;p&gt;The panel before a run reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01 Source&lt;/strong&gt; - Clear, Paste local data, Browse files, Input limits and privacy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02 Options&lt;/strong&gt; - a Delimiter picker shipping on &lt;code&gt;Auto detect&lt;/code&gt;, and one sentence that explains the whole mapping: "Each row becomes one object keyed by the header row."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03 Result&lt;/strong&gt; - empty, showing "Ready"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a comma-separated file loaded, the delimiter line reads &lt;code&gt;Auto detect · comma&lt;/code&gt;. So the header row becomes the keys, and the header row itself is not an object: one header line plus three data lines gives three objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  A run, with awkward values on purpose
&lt;/h2&gt;

&lt;p&gt;I ran it on a six-column CSV with three rows, deliberately including a decimal, a number, a lowercase boolean, an uppercase boolean, a leading-zero string, a date and an empty cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;
&lt;span class="k"&gt;Ada&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;engineer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;42.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;Grace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;engineer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;007&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;Linus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;maintainer&lt;/span&gt;&lt;span class="p"&gt;,,&lt;/span&gt;&lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2026-11-03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result panel reported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Converted 3 rows to JSON."&lt;/li&gt;
&lt;li&gt;Rows converted: 3&lt;/li&gt;
&lt;li&gt;Size: 401 B&lt;/li&gt;
&lt;li&gt;Lines: 26&lt;/li&gt;
&lt;li&gt;Converted in 174 ms&lt;/li&gt;
&lt;li&gt;Format: JSON&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;Convert again&lt;/code&gt; button&lt;/li&gt;
&lt;li&gt;a GENERATED OUTPUT block listing the lines, plus &lt;code&gt;Download CSV&lt;/code&gt; and &lt;code&gt;Download JSON&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every line above is what the panel printed in the tab, and the 401 B is the file the &lt;code&gt;Download JSON&lt;/code&gt; button wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output bytes
&lt;/h2&gt;

&lt;p&gt;The downloaded file was named &lt;code&gt;data.json&lt;/code&gt;, and this is its whole content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42.5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"007"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Linus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maintainer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRUE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five things are worth reading straight off it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;42.5&lt;/code&gt; is the string &lt;code&gt;"42.5"&lt;/code&gt;.&lt;/strong&gt; The converter does not infer types. If you need numbers, cast them downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;TRUE&lt;/code&gt; are all strings.&lt;/strong&gt; No boolean is inferred either, and the case is preserved exactly as it was in the cell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;007&lt;/code&gt; survives as &lt;code&gt;"007"&lt;/code&gt;.&lt;/strong&gt; Nothing is rounded, trimmed of leading zeros, or reformatted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;2026-11-03&lt;/code&gt; stays a string&lt;/strong&gt;, not a date. The tool has no date type and does not guess one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The empty cell becomes &lt;code&gt;""&lt;/code&gt;, not &lt;code&gt;null&lt;/code&gt;.&lt;/strong&gt; If the thing consuming your JSON treats an empty string and &lt;code&gt;null&lt;/code&gt; differently, that difference shows up here.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The line count has one honest wrinkle: the panel says 26 lines, and &lt;code&gt;wc -l&lt;/code&gt; on the file says 25. Same file. The panel counts the last line, which has no trailing newline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CSV export guards formulas
&lt;/h2&gt;

&lt;p&gt;One line in the result panel I did not expect: "Formula protection applies when exporting CSV."&lt;/p&gt;

&lt;p&gt;So I tested it, with a CSV whose cells begin with the three characters spreadsheets treat as the start of a formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;note&lt;/span&gt;
&lt;span class="k"&gt;Ada&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;Bob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;A&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;Cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kp"&gt;@cmd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Download CSV&lt;/code&gt; button wrote a file named &lt;code&gt;data-processed.csv&lt;/code&gt;, 62 bytes, every field quoted, CRLF line endings, and every formula-looking value prefixed with an apostrophe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"note"&lt;/span&gt;
&lt;span class="nv"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"'=1+1"&lt;/span&gt;
&lt;span class="nv"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"'+SUM(A1)"&lt;/span&gt;
&lt;span class="nv"&gt;"Cid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"'@cmd"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a cell that would be a live formula in Excel or Sheets comes out of the export inert. That protection is applied on the CSV export; the panel attributes it there specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;"Processing stays in this browser." - that is the line under the result, and it is the accurate description: the paste, the parse, the conversion and the download all happen in the tab.&lt;/p&gt;

&lt;p&gt;The plan block on the same page reads "Standard is free right now", "$0 / month", "100,000 credits every 30 days", "No end date set" and "No card required". Nothing about a file being uploaded is involved here, so the only thing you need is the tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it
&lt;/h2&gt;

&lt;p&gt;Use this tab when the shape of your data is a list of records with a header row, and you want JSON you can trust to be faithful to the cells. Do not reach for it when you need type coercion, nested objects, date parsing, or a JSON Schema - it does none of those, and it does not pretend to.&lt;/p&gt;

&lt;p&gt;If a downstream step needs numbers where you have strings, that is your cast to write, not the converter's guess to make.&lt;/p&gt;

&lt;p&gt;I work on OnePassApps, the team behind app.onepassapps.com. The Data Studio also does CSV viewing, sorting and filtering, de-duplicating rows, merging, splitting, YAML and JSON formatting, if you want the same trust in those.&lt;/p&gt;

</description>
      <category>csv</category>
      <category>json</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Split a CSV in the Browser: Row Counts, Column Values and the Bytes You Get</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:16:29 +0000</pubDate>
      <link>https://dev.to/onepassapps/split-a-csv-in-the-browser-row-counts-column-values-and-the-bytes-you-get-392c</link>
      <guid>https://dev.to/onepassapps/split-a-csv-in-the-browser-row-counts-column-values-and-the-bytes-you-get-392c</guid>
      <description>&lt;p&gt;Full disclosure: I work on OnePassApps.&lt;/p&gt;

&lt;p&gt;Splitting a big CSV into smaller files usually starts with "upload it here" - and the file usually holds names, emails or order records you would rather not hand to a stranger's server.&lt;/p&gt;

&lt;p&gt;The Data Studio at app.onepassapps.com/studios/data has a &lt;strong&gt;Split&lt;/strong&gt; tab that does it inside the browser tab. The page says where the work happens: &lt;em&gt;Nothing leaves this tab.&lt;/em&gt; and &lt;em&gt;Processing stays in this browser.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to cut the file
&lt;/h2&gt;

&lt;p&gt;The panel runs in three steps - &lt;strong&gt;01 Source&lt;/strong&gt;, &lt;strong&gt;02 Options&lt;/strong&gt;, &lt;strong&gt;03 Result&lt;/strong&gt; - and the Options step has two mode buttons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;By row count&lt;/strong&gt; - you set how many rows go into each part. The &lt;strong&gt;Rows per file&lt;/strong&gt; field ships at 1000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By column value&lt;/strong&gt; - the rows are grouped by the value in one column, and each distinct value becomes a part.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;strong&gt;Delimiter&lt;/strong&gt; control sits above the two modes and ships as &lt;strong&gt;Auto detect&lt;/strong&gt;. After a run the result panel reports what it found, so you can see the assumption instead of guessing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  By row count, checked on a real file
&lt;/h2&gt;

&lt;p&gt;I pasted a four-row CSV (name, role, city) into the source box, set &lt;strong&gt;Rows per file&lt;/strong&gt; to 2 and pressed &lt;strong&gt;Parse and split CSV&lt;/strong&gt;. The result read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Created 2 local CSV parts.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 rows · 3 columns · no parsing issues&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delimiter found&lt;/strong&gt; - &lt;strong&gt;Comma&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parts 2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split in 768 ms&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stage then showed &lt;strong&gt;SPLIT CSV&lt;/strong&gt;, &lt;strong&gt;Part 1 of 2&lt;/strong&gt;, &lt;strong&gt;2 rows · 3 columns&lt;/strong&gt;, the two parts in a list (&lt;strong&gt;Part 1&lt;/strong&gt;, &lt;strong&gt;Part 2&lt;/strong&gt;), and a &lt;strong&gt;Download selected part&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;The tool states its own ceiling in the Options step: &lt;em&gt;Each part becomes its own local CSV download. At most 100 parts are created per run.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  By column value, checked the same way
&lt;/h2&gt;

&lt;p&gt;Switching to &lt;strong&gt;By column value&lt;/strong&gt; adds a &lt;strong&gt;Group by&lt;/strong&gt; control, which ships on the first column (name). With a four-row file where every name is different, each distinct value became its own part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Created 4 local CSV parts.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 rows · 3 columns · no parsing issues&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parts 4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split in 443 ms&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parts were labelled by the value they hold - &lt;strong&gt;name = Ada&lt;/strong&gt;, &lt;strong&gt;name = Grace&lt;/strong&gt;, &lt;strong&gt;name = Linus&lt;/strong&gt;, &lt;strong&gt;name = Mika&lt;/strong&gt; - and the stage showed &lt;strong&gt;Part 1 of 4&lt;/strong&gt; with &lt;strong&gt;name = Ada · 1 row · 3 columns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One honest limit: I could not get the &lt;strong&gt;Group by&lt;/strong&gt; control to open a list of columns in the tab, so my evidence for this mode is the default column only. How the tool behaves when the column you group by repeats across rows is therefore something I have not measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bytes that come out
&lt;/h2&gt;

&lt;p&gt;The interesting part of a split is the file, not the counter. I clicked &lt;strong&gt;Download selected part&lt;/strong&gt; on the &lt;code&gt;name = Ada&lt;/code&gt; part and captured the blob the tab handed to the download:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;type &lt;code&gt;text/csv;charset=utf-8&lt;/code&gt;, 47 bytes&lt;/li&gt;
&lt;li&gt;content: &lt;code&gt;"name","role","city"&lt;/code&gt; then &lt;code&gt;"Ada","engineer","Berlin"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;line ending CRLF, and every field quoted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a part is a real CSV file with a header row - not a slice of text and not a picture of a table. In both runs the &lt;strong&gt;Parts&lt;/strong&gt; counter matched the number of entries in the parts list (2 and 4), and the stage offered &lt;strong&gt;Download selected part&lt;/strong&gt; next to a &lt;strong&gt;Whole CSV&lt;/strong&gt; label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;The Data Studio row of tools is Viewer, Sort / filter, Columns, Duplicates, Merge, Split, To JSON, Formatter, Validator, To CSV and YAML - so you can reshape the same file and export it without moving it between websites.&lt;/p&gt;

&lt;p&gt;Standard is currently free during a promotional period: $0 per month, 100,000 credits every 30 days, no card required. The tools run in the browser tab, so download your parts before you close it.&lt;/p&gt;

&lt;p&gt;Try it on your own file: &lt;a href="https://app.onepassapps.com/studios/data?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=split-csv-parts&amp;amp;utm_content=data-studio-link" rel="noopener noreferrer"&gt;open Data Studio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want the wider picture on how these browser-local tools work and what they do not do, the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=split-csv-parts&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps guides&lt;/a&gt; cover it.&lt;/p&gt;

&lt;p&gt;OnePassApps Team - we build app.onepassapps.com, one membership for nine browser-local and hosted apps.&lt;/p&gt;

</description>
      <category>csv</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>Remove Duplicate Lines in the Browser: Case, Empty Lines and Which Copy You Keep</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 04:59:19 +0000</pubDate>
      <link>https://dev.to/onepassapps/remove-duplicate-lines-in-the-browser-case-empty-lines-and-which-copy-you-keep-4b77</link>
      <guid>https://dev.to/onepassapps/remove-duplicate-lines-in-the-browser-case-empty-lines-and-which-copy-you-keep-4b77</guid>
      <description>&lt;p&gt;I work on OnePassApps. We build browser studios that do their work in the tab instead of uploading your files, and Text Studio is the one for plain text: eight tools on one tab bar - Clean, Case, Duplicate lines, Sort lines, Find &amp;amp; replace, Diff, Markdown and Base64.&lt;/p&gt;

&lt;p&gt;The Duplicate lines tab is the one that looks trivial until you ask what "duplicate" means. Is &lt;code&gt;Apple&lt;/code&gt; the same line as &lt;code&gt;apple&lt;/code&gt;? Is &lt;code&gt;" apple"&lt;/code&gt; (with a leading space) the same as &lt;code&gt;apple&lt;/code&gt;? When a line repeats three times, which copy survives, and where does it end up in the output? This is about those rules rather than the button: the three options, and what the tool actually does with case, empty lines and repeated lines.&lt;/p&gt;

&lt;p&gt;The page describes itself in one line: "Count, clean, compare and convert text locally. Your content stays in this browser tab."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tab gives you
&lt;/h2&gt;

&lt;p&gt;One panel, three steps. &lt;strong&gt;01 Input text&lt;/strong&gt; - a box with a live counter (2,000,000 characters on this tab) and a live stats row: Words, Characters, Sentences, Lines and UTF-8 bytes. &lt;strong&gt;02 Options&lt;/strong&gt; - three controls: Keep duplicate order, Case-sensitive and Keep every empty line. &lt;strong&gt;03 Result&lt;/strong&gt; - "Unique lines shows on the stage, ready to copy, download or use as the new input."&lt;/p&gt;

&lt;p&gt;The action button reads "Remove duplicate lines" and becomes "Remove again" once a run has happened. The result panel then reports the size of what it produced and how long it took ("Duplicates removed in 0.1 s"), and offers Use as input, Download .txt and Copy. The stage footer reads "Text transformed locally." and the panel says "Nothing leaves this tab."&lt;/p&gt;

&lt;p&gt;You can drop a file in instead of pasting: the drop area takes ".txt, .md, .csv and other text up to 8 MiB", and the tab says "Processing stays in this browser." The file input itself accepts &lt;code&gt;.txt,.text,.md,.markdown,.csv,.tsv,.json,.log,.xml,.yaml,.yml,.html,.srt,.vtt&lt;/code&gt;, plus the &lt;code&gt;text/plain&lt;/code&gt;, &lt;code&gt;text/markdown&lt;/code&gt; and &lt;code&gt;text/csv&lt;/code&gt; types.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep duplicate order&lt;/strong&gt; is a dropdown with two entries: "First occurrence" and "Last occurrence". It ships on First occurrence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case-sensitive&lt;/strong&gt; ships on (ticked), and &lt;strong&gt;Keep every empty line&lt;/strong&gt; ships on (ticked) as well. Those two defaults are the whole reason the tab is worth a look: a default-on case-sensitive match will happily keep &lt;code&gt;Apple&lt;/code&gt; and &lt;code&gt;apple&lt;/code&gt; as two different lines, and a default-on empty-line setting keeps your blank lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 1: the defaults
&lt;/h2&gt;

&lt;p&gt;Input, eight lines including one empty line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
banana
apple
Apple
 apple
banana

apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the defaults (First occurrence, Case-sensitive on, Keep every empty line on), the result stage reported "Duplicates removed in 0.1 s" and "26 characters · 5 lines". The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
banana
Apple
 apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that carefully, because every line in it is a decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apple&lt;/code&gt; appears three times in the input and survives &lt;strong&gt;once&lt;/strong&gt;. The first occurrence won, so the output keeps the lowercase spelling, not the capitalised one that came later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Apple&lt;/code&gt; survives as its own line. Case-sensitive is on, so it is a different line from &lt;code&gt;apple&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apple&lt;/code&gt; (with a leading space) survives as its own line too. The comparison is on the exact line, so a leading space makes it different.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;banana&lt;/code&gt; appears twice and survives once.&lt;/li&gt;
&lt;li&gt;The empty line survives, because Keep every empty line is on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run 2: case-insensitive
&lt;/h2&gt;

&lt;p&gt;Untick Case-sensitive and run the same input again. The result is "20 characters · 4 lines":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
banana
 apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Apple&lt;/code&gt; is now folded into &lt;code&gt;apple&lt;/code&gt;, so the output has one fewer line. Note the leading-space line still stands on its own: the tool compares the line as it is, it does not trim whitespace before comparing. So "case-insensitive" here means exactly that - it changes the letter comparison and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 3: and the empty line goes too
&lt;/h2&gt;

&lt;p&gt;Untick Keep every empty line as well and the same input gives "19 characters · 3 lines":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
banana
 apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines now. The empty line is gone, the case difference is gone, and the whitespace difference is still there. Those are three separate switches doing three separate jobs, which is worth knowing before you assume "de-duplicate" means one particular thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 4: which copy survives, and where does it land
&lt;/h2&gt;

&lt;p&gt;Keep duplicate order decides both. With the input &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First occurrence&lt;/strong&gt; gives &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt; - the surviving copy keeps the &lt;em&gt;first&lt;/em&gt; position the line ever had.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last occurrence&lt;/strong&gt; gives &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt; - the surviving copy moves to the &lt;em&gt;last&lt;/em&gt; position that line appeared in, and the other lines shift up to close the gap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both runs report the same size, "5 characters · 3 lines". The set of unique lines is identical; only the order differs. That is the whole difference between the two dropdown entries.&lt;/p&gt;

&lt;p&gt;The same thing with a line that repeats three times, input &lt;code&gt;keep&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;keep&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;keep&lt;/code&gt;, under Last occurrence, gives &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;keep&lt;/code&gt;: the surviving copy lands at the end, where the third occurrence was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output file
&lt;/h2&gt;

&lt;p&gt;The result panel offers "Download .txt", and that button really writes a file. Reading the blob it creates back gives a &lt;code&gt;text/plain;charset=utf-8&lt;/code&gt; payload of exactly 26 bytes for the first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
banana
Apple
 apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;26 bytes, matching the "26 characters · 5 lines" the panel reported. The trailing newline on the last line is in the file, and the empty line from the input is not in it as an empty line - it is the newline that separates &lt;code&gt;apple&lt;/code&gt; from the end of the file, which is why the panel counts 5 lines while the text shows 4 visible ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tab does not do
&lt;/h2&gt;

&lt;p&gt;It does not sort. The output comes out in the order the surviving lines were in the input, and the Sort lines tab is the separate tool for reordering. It does not trim or normalise whitespace, and it does not rewrite the lines it keeps - the output is made of the exact strings that were in the input.&lt;/p&gt;

&lt;p&gt;It also does not touch the original. "Use as input" moves the result into the input box so you can run another tool on it, and your pasted text or your file on disk is never modified.&lt;/p&gt;

&lt;p&gt;Try it on the &lt;a href="https://app.onepassapps.com/studios/text?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=text-duplicate-lines&amp;amp;utm_content=text-studio-link" rel="noopener noreferrer"&gt;Text Studio&lt;/a&gt;, and the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=text-duplicate-lines&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;guides page&lt;/a&gt; covers the rest of the studios. The standard plan is free right now - $0/month, 100,000 credits every 30 days, no card required.&lt;/p&gt;

</description>
      <category>text</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>Build a PWA Icon Pack in the Browser: 8 Sizes, a Manifest and a README</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 04:41:53 +0000</pubDate>
      <link>https://dev.to/onepassapps/build-a-pwa-icon-pack-in-the-browser-8-sizes-a-manifest-and-a-readme-23mh</link>
      <guid>https://dev.to/onepassapps/build-a-pwa-icon-pack-in-the-browser-8-sizes-a-manifest-and-a-readme-23mh</guid>
      <description>&lt;p&gt;I work on OnePassApps. We build browser studios that do their work in the tab instead of uploading your files, and Icon Studio is the one for icons: eight tools on one tab bar - SVG to PNG, PNG to ICO, Favicon pack, App icons, Resize set, Optimize SVG, Recolour SVG and Trim / padding.&lt;/p&gt;

&lt;p&gt;The App icons tab is the one that produces a whole installable icon set rather than a single file: one image in, eight PWA sizes plus a manifest and a README, packed into one ZIP. So this is about what actually lands in that archive, and about what the tab does not do.&lt;/p&gt;

&lt;p&gt;The page describes itself in one line: "Create icons and favicon assets on this device. Files stay in your active browser tab."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tab gives you
&lt;/h2&gt;

&lt;p&gt;One panel, three steps. &lt;strong&gt;01 Source&lt;/strong&gt; - "No SVG or PNG yet. Drop one on the stage or browse for it. SVG up to 5 MiB, PNG up to 24 MiB." &lt;strong&gt;02 Pack details&lt;/strong&gt; - "Name the app for the manifest once an image is open. The pack holds 8 PWA sizes, 72 to 512 pixels." &lt;strong&gt;03 Result&lt;/strong&gt; - "A ZIP with 8 PWA PNG sizes, a manifest and a README."&lt;/p&gt;

&lt;p&gt;The action button reads "Create app icon pack". Once an image is open the panel fills in: the file name with its type and dimensions, "8 sizes, 72 to 512 px", a Purpose field that read "any" in this run, and an "Also included" line reading "Manifest, README". The only text field is the app name for the manifest, and it read "App" in this run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;The source was a 512 x 512 PNG of 2.2 KiB, written by a probe script and dropped on the stage. The result panel reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;probe-appicons-pwa-icons.zip
10 files - 18.6 KiB
Before   2.2 KiB  PNG
After    18.6 KiB ZIP
Created in 0.6 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the eight icons, listed by size and file name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;72 x 72     icon-72x72.png
96 x 96     icon-96x96.png
128 x 128   icon-128x128.png
144 x 144   icon-144x144.png
152 x 152   icon-152x152.png
192 x 192   icon-192x192.png
384 x 384   icon-384x384.png
512 x 512   icon-512x512.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And two more entries under "ALSO IN THE ZIP":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site.webmanifest   333 B
README.txt         109 B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README is a single line, shown on the panel verbatim: "PWA PNG icon sizes 72, 96, 128, 144, 152, 192, 384, 512 were created with transparent backgrounds."&lt;/p&gt;

&lt;p&gt;Ten files in the archive: eight icons, a manifest, a README. The panel also offers a Dark and a Light preview next to the ZIP, so you can see the icon against both backgrounds before you take the file.&lt;/p&gt;

&lt;p&gt;The archive name follows the file you dropped in: the source was probe-appicons.png and the ZIP came out as probe-appicons-pwa-icons.zip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this tab does not do
&lt;/h2&gt;

&lt;p&gt;It does not draw the icons. You supply one SVG or PNG and the pack resizes that image - the artwork is yours.&lt;/p&gt;

&lt;p&gt;I read the manifest's size on the panel (333 B) but did not unpack the archive in this run, so I am not going to describe what is inside that JSON beyond the byte count.&lt;/p&gt;

&lt;p&gt;The app name field is the only free-text input, and it feeds the manifest. In this run it read "App" because that is exactly what the probe typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;Icon Studio is part of OnePassApps and it lives on the &lt;a href="https://app.onepassapps.com/studios/icons?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=app-icon-pack&amp;amp;utm_content=icon-studio-link" rel="noopener noreferrer"&gt;Icon Studio page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everything happens in the tab: the image is read in the browser, the sizes are rendered there, and the ZIP is written out on your own machine. Standard is free right now - $0/month, 100,000 credits every 30 days, no card required, and no automatic paid subscription.&lt;/p&gt;

&lt;p&gt;Guides for the studios and the apps behind them are on the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=app-icon-pack&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps guides page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>icons</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Sort Lines in the Browser: Numbers, Case and the Whitespace That Decides the Order</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 03:54:32 +0000</pubDate>
      <link>https://dev.to/onepassapps/sort-lines-in-the-browser-numbers-case-and-the-whitespace-that-decides-the-order-3kg2</link>
      <guid>https://dev.to/onepassapps/sort-lines-in-the-browser-numbers-case-and-the-whitespace-that-decides-the-order-3kg2</guid>
      <description>&lt;p&gt;I work on OnePassApps. We build browser studios that do their work in the tab instead of uploading your files, and Text Studio is the one for plain text: eight tools on one tab bar - Clean, Case, Duplicate lines, Sort lines, Find &amp;amp; replace, Diff, Markdown and Base64.&lt;/p&gt;

&lt;p&gt;The Sort lines tab is the one that looks trivial until you feed it a list that is not a plain list of names. Version strings, IDs typed as text, a column of numbers - each of those has a different idea of what "sorted" means. So this is about the ordering rules rather than the button: what the three options do, and what the tool does with spaces, blank lines and duplicates.&lt;/p&gt;

&lt;p&gt;The page describes itself in one line: "Count, clean, compare and convert text locally. Your content stays in this browser tab."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tab gives you
&lt;/h2&gt;

&lt;p&gt;One panel, three steps. &lt;strong&gt;01 Input text&lt;/strong&gt; - a box with a live counter (2,000,000 characters on this tab) and a live stats row: Words, Characters, Sentences, Lines and UTF-8 bytes. &lt;strong&gt;02 Options&lt;/strong&gt; - three controls: Sort order (A to Z or Z to A), Case-sensitive and Natural number order. &lt;strong&gt;03 Result&lt;/strong&gt; - "Sorted lines shows on the stage, ready to copy, download or use as the new input."&lt;/p&gt;

&lt;p&gt;The action button reads "Sort lines" and becomes "Sort again" once a run has happened. The result panel then reports the size of what it produced and how long it took ("Sorted in 0.2 s"), and offers Use as input, Download .txt and Copy. The stage footer reads "Text transformed locally." and the panel says "Nothing leaves this tab."&lt;/p&gt;

&lt;p&gt;You can drop a file in instead of pasting: the drop area takes ".txt, .md, .csv and other text up to 8 MiB", and the tab says "Processing stays in this browser."&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 1 - what the defaults do with numbers
&lt;/h2&gt;

&lt;p&gt;Input (6 lines, 33 characters - the stats row read Words 7, Characters 33, Sentences 0, Lines 6, UTF-8 bytes 33):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;banana
Apple
10
9
cherry
2 apples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Options left where they ship: Sort order A to Z, Case-sensitive off, Natural number order on. Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 apples
9
10
Apple
banana
cherry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happened. The numbers came first and in numeric order - 9 before 10, not "10" before "9" the way a plain text sort does it. And "2 apples", which starts with a digit, was treated as a number and sorted into that group: 2, then 9, then 10. Everything that does not start with a digit came after, alphabetically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 2 - the same input with Natural number order switched off
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10
2 apples
9
Apple
banana
cherry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is plain lexicographic order: "10" first because the character "1" comes before "2", and "2 apples" before "9". If your list is numbers stored as text, this is the difference between a useful order and a broken one. Natural number order is on by default, which is the right default for a list of IDs, and the one to turn off for version strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 3 - Case-sensitive, and what the toggle actually changes
&lt;/h2&gt;

&lt;p&gt;The checkbox sits under Sort order and ships off. Here is the same input sorted twice.&lt;/p&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zebra
apple
Apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Case-sensitive off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
Apple
Zebra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Case-sensitive on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
Apple
Zebra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same output - because the only case difference in this sample is between "apple" and "Apple", and with the comparison turned off those two compare equal, so their original order is kept. Here is a sample where the toggle does change the answer:&lt;/p&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
Banana
banana
apple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Case-sensitive off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
apple
Banana
banana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Case-sensitive on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple
apple
banana
Banana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now "Banana" and "banana" are no longer tied. The lower-case one came first. So the toggle is not "upper case before lower case" - it stops treating two spellings as the same value, and the order it then picks put the lower-case spelling first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run 4 - whitespace, blank lines and duplicates are left alone
&lt;/h2&gt;

&lt;p&gt;Input (9 lines, 46 characters) - note the leading spaces on the first line, the two empty lines, the trailing spaces after "trailing", the repeated "line", and both spellings of "lead":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  lead
line


second
trailing  
line
line
Lead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

  lead
Lead
line
line
line
second
trailing  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things to notice. The empty lines did not disappear - they sorted to the top, because an empty string sorts before any letter, and they are still in the output. The leading spaces on "  lead" are kept, and they are why that line sorts above "Lead": a space sorts before a letter. The trailing spaces on "trailing" are kept in the output too. And the three "line" entries are all still there - Sort lines reorders, it never de-duplicates; removing duplicates is the separate Duplicate lines tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bit that will bite you
&lt;/h2&gt;

&lt;p&gt;Sorting is a comparison, and the comparison is not your editor's. Two things to watch. Whitespace is part of the value unless you clean it first - the Clean tab removes trailing spaces, trims outer whitespace and collapses repeated blank lines, so run it first on a list that came out of a copy-paste. And lines that compare equal keep their original order instead of being shuffled, which is why the two "apple" lines stay in place in the run above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;Text Studio is part of OnePassApps and it lives on the &lt;a href="https://app.onepassapps.com/studios/text?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=text-sort-lines&amp;amp;utm_content=text-studio-link" rel="noopener noreferrer"&gt;Text Studio page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The whole thing happens in the tab: the file is read in the browser, the lines are compared there, and the sorted copy is written out on your own machine. Standard is free right now - $0/month, 100,000 credits every 30 days, no card required, and no automatic paid subscription.&lt;/p&gt;

&lt;p&gt;Guides for the studios and the apps behind them are on the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=text-sort-lines&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps guides page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>text</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>How the OnePassApps Vote Board Counts: Ties, Ordering and the Sign-In Gate</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 03:28:23 +0000</pubDate>
      <link>https://dev.to/onepassapps/how-the-onepassapps-vote-board-counts-ties-ordering-and-the-sign-in-gate-54nl</link>
      <guid>https://dev.to/onepassapps/how-the-onepassapps-vote-board-counts-ties-ordering-and-the-sign-in-gate-54nl</guid>
      <description>&lt;p&gt;Most "tell us what to build next" widgets are a form in a corner with a text box under it. The OnePassApps voting board is a different shape of thing: a live board with five ideas on it, a vote count on every card, and a stated rule for what rises to the top.&lt;/p&gt;

&lt;p&gt;I work on OnePassApps, the team behind app.onepassapps.com, so I am going to describe how the board actually behaves - the ordering rule, the refresh, the sign-in gate - and exactly what each of the five ideas says it will do.&lt;/p&gt;

&lt;p&gt;Open the &lt;a href="https://app.onepassapps.com/voting?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=community-vote-board&amp;amp;utm_content=vote-board-link" rel="noopener noreferrer"&gt;community voting board&lt;/a&gt; to look at it yourself, or read the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=community-vote-board&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;app guides&lt;/a&gt; for the tools that are already live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three steps the board spells out
&lt;/h2&gt;

&lt;p&gt;The panel above the cards is a short explainer, and it is worth reading because it tells you what the page is asking of you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01 Browse ideas&lt;/strong&gt; - "Find the apps you'd use"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02 Use your votes&lt;/strong&gt; - "Back your favourites"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03 Set priorities&lt;/strong&gt; - "Top ideas rise first"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third line is the ordering rule in plain words. The board is not a poll where every idea is fixed in place; support is what moves a card.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules the board prints about itself
&lt;/h2&gt;

&lt;p&gt;Four short strings on the page do the real explaining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"5 ideas" - the count of cards currently on the board.&lt;/li&gt;
&lt;li&gt;"Updates every 30 seconds" - the refresh interval for the vote counts, so you can leave the page open and watch a card climb without reloading.&lt;/li&gt;
&lt;li&gt;"TIED AT 0 VOTES" - the tie marker, with the tiebreak spelled out next to it: "IN THE ORDER ADDED". When nothing has any support yet, the board does not hide that fact; it shows the tie and then breaks it by insertion order, so the list you see is stable rather than arbitrary.&lt;/li&gt;
&lt;li&gt;"Top ideas rise first" - the sort rule that follows from the votes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a "YOUR VOTES" panel with the line "Back an idea, or bring your own", and the count of ideas on the board sits next to the label "Ideas in the running".&lt;/p&gt;

&lt;h2&gt;
  
  
  Voting needs an account, and that is deliberate
&lt;/h2&gt;

&lt;p&gt;A visitor can browse the board. Voting does not work for a visitor - the page says "Sign in to vote for the ideas you'd use. Pro also lets you submit an app idea of your own", and the button on the cards reads "Sign in to vote".&lt;/p&gt;

&lt;p&gt;That gate is the point of the board rather than an obstacle to it. A vote is only useful if it is tied to a person, otherwise the ordering rule is trivially gameable. The same panel explains the second half of the loop: if none of the five is the thing you would use, the board takes a new idea. At the bottom of the list there is a "Have an idea?" block with a "Suggest an app" button.&lt;/p&gt;

&lt;p&gt;So the loop is: browse the running ideas, back the ones you would actually use, and if the board has nothing for you, put your own idea on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five ideas on the board right now
&lt;/h2&gt;

&lt;p&gt;These are the cards currently on the board, with the descriptions the board gives them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ReadAloud&lt;/strong&gt; - "A dedicated app for listening to supplied text with clear playback and reading controls."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SubRadar&lt;/strong&gt; - "Track recurring subscriptions in one place, spot overlapping AI tools, and get clear suggestions about what to cancel or keep."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DumpList&lt;/strong&gt; - "Turn a quick voice dump into structured tasks, due dates, and optional calendar blocks."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NoteQuiz&lt;/strong&gt; - "Turn a photo, handwritten page, or PDF into flashcards, a ten-question quiz, and a recap of what you keep missing."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FiveMin&lt;/strong&gt; - "Pick topics and creators, then get a concise daily briefing written in your tone and ready to play as audio."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these five is a shipped app. They are the running list, and the board is where you would push back on them - a vote for ReadAloud and a suggestion of your own thing both end up in the same place, and the ordering rule decides which idea gets built first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually live today
&lt;/h2&gt;

&lt;p&gt;So the board is not the whole product, and I would rather say what exists than let the board imply there is nothing else. Nine apps are live now under one membership, and Standard is free at the moment with no card needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MovieMatcherAI&lt;/strong&gt; - browse, rate and keep a watchlist. The Browse tab lists 89 titles, with filters for new releases, top rated, rating, genre, year and person; ratings and watchlist sync to an account, so signing in is what makes the list yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NewsFlow&lt;/strong&gt; - server-side news briefs with source attribution and a link back to the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven browser Studios&lt;/strong&gt; - Image, Audio, File, Text, Data, Icon and Colour. They process the file in the tab instead of uploading it, which is the same idea as the vote board in a different place: the thing happens where the person is, and the file never leaves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Studios are the part most people come for, and they are the part I have written most about. The voting board is the part I would point a developer at, because the interesting detail is in the rules: a tie shown honestly instead of hidden, a stated tiebreak, a refresh interval you can plan around, and a sign-in requirement that makes the ordering mean something.&lt;/p&gt;

&lt;p&gt;If one of the five is the app you would use, back it. If it is not, put your own idea on the board - the "Suggest an app" button is right there under the list.&lt;/p&gt;

&lt;p&gt;Disclosure again: I work on OnePassApps, the team behind app.onepassapps.com.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tools</category>
      <category>productivity</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Markdown Preview in the Browser: What the Sanitiser Drops Before You Copy</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 03:00:25 +0000</pubDate>
      <link>https://dev.to/onepassapps/markdown-preview-in-the-browser-what-the-sanitiser-drops-before-you-copy-21lo</link>
      <guid>https://dev.to/onepassapps/markdown-preview-in-the-browser-what-the-sanitiser-drops-before-you-copy-21lo</guid>
      <description>&lt;p&gt;Most "markdown preview" boxes on the web do one of two things: they render your Markdown and then quietly fetch every image and follow every link, or they escape the raw HTML and hand you a wall of &lt;code&gt;&amp;amp;lt;&lt;/code&gt; characters. OnePassApps' Text Studio takes a third route: it renders the Markdown, strips the dangerous parts, and shows you exactly what is left before you copy it.&lt;/p&gt;

&lt;p&gt;I wanted to know what survives that strip, so I ran the tab against a file containing a heading, emphasis, a table, a fenced code block, a link, an image, a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag and an inline &lt;code&gt;&amp;lt;b onclick=...&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on OnePassApps.&lt;/p&gt;

&lt;p&gt;Open the &lt;a href="https://app.onepassapps.com/studios/text?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=markdown-sanitiser&amp;amp;utm_content=text-studio-link" rel="noopener noreferrer"&gt;Text Studio&lt;/a&gt; to try it, or read the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=markdown-sanitiser&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;app guides&lt;/a&gt; for the rest of the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tab, and its smaller input limit
&lt;/h2&gt;

&lt;p&gt;Text Studio has eight tools in one tab strip: Clean, Case, Duplicate lines, Sort lines, Find &amp;amp; replace, Diff, Markdown and Base64. The Markdown tab is the eighth.&lt;/p&gt;

&lt;p&gt;The page line over the strip reads "Count, clean, compare and convert text locally. Your content stays in this browser tab." The file picker accepts ".txt, .md, .csv and other text up to 8 MiB · or paste it in Input text · nothing is uploaded", and the page repeats "Nothing leaves this tab." and "Processing stays in this browser."&lt;/p&gt;

&lt;p&gt;One detail worth knowing before you paste: the input counter on the Markdown tab reads &lt;strong&gt;250,000&lt;/strong&gt;, not 2,000,000. The Clean tab's counter reads 2,000,000. So the Markdown tab has a lower ceiling than its neighbours, and it is the tab where a ceiling matters, because a rendered preview is a heavier operation than a line-ending fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel says what it does before you press anything
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01 Input text&lt;/strong&gt; — the counter, plus a live Words / Characters / Sentences / Lines / UTF-8 bytes row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02 Preview&lt;/strong&gt; — "Markdown renders in this tab without opening links or loading images and scripts. Copy the sanitised HTML when it looks right."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03 Result&lt;/strong&gt; — "The preview is sanitised: links, images and scripts are never loaded."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the contract, and it is worth reading carefully because the middle sentence is narrower than it looks. "Links" are not blocked. Links are &lt;em&gt;stripped of their targets&lt;/em&gt;. That is a different thing, and the difference shows up in the output.&lt;/p&gt;

&lt;p&gt;The action button reads "Preview Markdown". After a run it becomes "Preview again", and the result panel reports "Ready" and "Rendered in" a fraction of a second, with a "Copy HTML" button and a line under the heading: "Safe preview · links, images and scripts are not loaded".&lt;/p&gt;

&lt;h2&gt;
  
  
  The input I used
&lt;/h2&gt;

&lt;p&gt;Nothing exotic — one heading, one paragraph, a list, a table, a code fence, a link, an image, a script tag and an inline HTML tag with an event handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Heading one&lt;/span&gt;

A paragraph with &lt;span class="gs"&gt;**bold**&lt;/span&gt;, &lt;span class="ge"&gt;*italic*&lt;/span&gt;, &lt;span class="sb"&gt;`inline code`&lt;/span&gt; and a &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;span class="p"&gt;
-&lt;/span&gt; first item
&lt;span class="p"&gt;-&lt;/span&gt; second item

| name | role |
| --- | --- |
| Ada | engineer |

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;js
&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;alert('xss')&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;b&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;raw html&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;alt text&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/a.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counter read 322 characters, 48 words, 20 lines and 322 UTF-8 bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;Here is the markup that "Copy HTML" put on the clipboard, 427 characters, exactly as it was written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Heading one&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;A paragraph with &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;bold&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;, &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;italic&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&lt;/span&gt;, &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;inline code&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt; and a &lt;span class="nt"&gt;&amp;lt;a&amp;gt;&lt;/span&gt;link&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;first item&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;second item&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;name&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;role&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;tbody&amp;gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;Ada&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;engineer&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/tbody&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&amp;lt;code&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"language-js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;const x = 1 &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt; 2 &lt;span class="ni"&gt;&amp;amp;amp;&amp;amp;amp;&lt;/span&gt; 3 &lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt; 2;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;b&amp;gt;&lt;/span&gt;raw html&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The button confirms it with "Sanitised HTML copied to the clipboard." and the visible preview is the same markup, rendered in the page under the label "Safe preview · links, images and scripts are not loaded".&lt;/p&gt;

&lt;p&gt;Read that block line by line, because each line is a different rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headings, emphasis, lists, tables and code fences all survive, intact.&lt;/strong&gt; The table is real table markup, with a thead row and a tbody row. The fenced code block keeps its language class — the opening tag carries the class "language-js" — and the code inside is HTML-escaped the way it should be, so &lt;code&gt;1 &amp;lt; 2 &amp;amp;&amp;amp; 3 &amp;gt; 2&lt;/code&gt; comes out as &lt;code&gt;1 &amp;amp;lt; 2 &amp;amp;amp;&amp;amp;amp; 3 &amp;amp;gt; 2&lt;/code&gt; instead of being parsed as markup. That escaping is why the code block is safe to paste into a page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The script tag is gone.&lt;/strong&gt; Not escaped, not shown as text — gone. There is no script element in the output and no trace of the alert call. The one line it occupied is simply absent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The link keeps its text and loses its target.&lt;/strong&gt; The anchor element is still there, still wrapping the word "link" — but it has no href attribute at all. It is not a link to &lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt; any more; it is a piece of text that used to be a link. That is what "links are not loaded" means in practice, and it is the difference between a preview that is safe to paste and one that quietly carries live URLs out of the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The image disappears entirely.&lt;/strong&gt; The markdown image produced no image element and no alt text — just an empty paragraph. So there is nothing to load, and the stripped line leaves a blank paragraph behind it. Worth knowing if you diff the result against your source: an image line becomes an empty paragraph, not a removed line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw HTML is kept but stripped of its attributes.&lt;/strong&gt; The inline bold tag came through as a bold element with the text "raw html" in it, and the onclick handler was dropped. So the sanitiser does not escape all HTML — it filters it. Tags with dangerous attributes lose those attributes; the safe parts of the tag survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for pasting the output
&lt;/h2&gt;

&lt;p&gt;The copy is a fragment, not a full document — no &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, no &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, no body wrapper. It starts at the first heading. That is what you want if you are pasting into a CMS, a README, an email composer or a comment box: markup, with the executable parts removed and the loadable parts removed.&lt;/p&gt;

&lt;p&gt;It is also not a round trip. Markdown in does not mean Markdown out — what you get is HTML, and the image is the clearest example of a source line that has no counterpart in the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;The plan block on the page reads: "FREE PROMOTIONAL PERIOD", "Standard is free right now", "$0 / month", "100,000 credits every 30 days · No end date set · No card required" and "No automatic paid subscription. Existing paid subscriptions keep their terms."&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;If you write Markdown in a tool and paste it into something else, the usual worry is that the paste carries a script, a tracking pixel, or a link that fires a request when someone opens it. This tab answers that worry by removing the targets rather than trusting them, and by showing you the exact output before you copy it — the copy button and the preview are the same bytes.&lt;/p&gt;

&lt;p&gt;It does not replace a linter or a sanitiser for content you did not write, and it does not render anything to a file: the preview lives in the tab, and "Copy HTML" is the hand-off.&lt;/p&gt;

&lt;p&gt;The rest of the Text Studio sits next to it: Clean, Case, Duplicate lines, Sort lines, Find &amp;amp; replace, Diff and Base64.&lt;/p&gt;

&lt;p&gt;Disclosure again: I work on OnePassApps, the team behind app.onepassapps.com.&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>webdev</category>
      <category>security</category>
      <category>tools</category>
    </item>
    <item>
      <title>Batch Rename Files in the Browser: The Preview Before the ZIP</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 02:08:49 +0000</pubDate>
      <link>https://dev.to/onepassapps/batch-rename-files-in-the-browser-the-preview-before-the-zip-28bp</link>
      <guid>https://dev.to/onepassapps/batch-rename-files-in-the-browser-the-preview-before-the-zip-28bp</guid>
      <description>&lt;p&gt;Rename a folder of 200 files and you find out three things fast: the naming convention you half-remember, the two names that would collide, and the fact that most web-based rename tools want the whole folder before they show you a single new name.&lt;/p&gt;

&lt;p&gt;The File Studio's &lt;strong&gt;Batch rename&lt;/strong&gt; tab in OnePassApps does the opposite. You drop the files in, type the rule, and a rename preview tells you exactly what each name becomes before anything is packed.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on OnePassApps.&lt;/p&gt;

&lt;p&gt;Open the &lt;a href="https://app.onepassapps.com/studios/files?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=batch-rename-preview-launch&amp;amp;utm_content=file-studio-link" rel="noopener noreferrer"&gt;File Studio&lt;/a&gt; to try it, or read the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=batch-rename-preview-launch&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;app guides&lt;/a&gt; for the rest of the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01 Source&lt;/strong&gt; — the files you picked. The counter starts at "0 files · 0 B." and the panel tells you what will come back: "Renamed copies are packed in one ZIP."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02 New names&lt;/strong&gt; — Prefix, Suffix before extension, Find in filename, Replace with, a Case conversion control, an "Add sequence number" checkbox and a "Starts at" field. The hint under the numbering is "Turn on numbering first."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03 Result&lt;/strong&gt; — "Create renamed copies ZIP packs the new names into renamed-copies.zip. Names on your device never change."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last sentence is the whole design. It makes copies; it does not touch your disk. The drop area says "Up to 500 files and 256 MiB · the names on your device never change", and the page repeats the two lines this Studio is built on: "Nothing leaves this tab." and "Processing stays in this browser."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rename preview actually shows
&lt;/h2&gt;

&lt;p&gt;The preview panel is headed RENAME PREVIEW and starts empty ("Nothing chosen yet"). Once files are loaded it lists, per file, the old name, the new name and the size.&lt;/p&gt;

&lt;p&gt;Here is a run I did on two real files, "Q3 Report.txt" and "notes.txt", five bytes each. I typed &lt;code&gt;2026-&lt;/code&gt; into the Prefix field and ticked "Add sequence number". The preview read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 files&lt;/li&gt;
&lt;li&gt;2 names change&lt;/li&gt;
&lt;li&gt;Q3 Report.txt → 2026-Q3 Report-1.txt&lt;/li&gt;
&lt;li&gt;notes.txt → 2026-notes-2.txt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at where the number lands: it is inserted before the extension, and it counts across the whole selection in order, so the second file gets 2 even though the sequence started at 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The collision rule
&lt;/h2&gt;

&lt;p&gt;Rename rules collide. Two different files can end up with the same new name, and whatever builds the archive has to resolve that or one file silently disappears.&lt;/p&gt;

&lt;p&gt;The panel states the rule in one line: "Name collisions are resolved in selection order with “(2)”, “(3)” and so on."&lt;/p&gt;

&lt;p&gt;So a name that is already taken in the pack gets (2) appended, then (3), and the order comes from the order you selected the files — not from alphabetical order. Selection order is the thing to control here, because that is what decides which file keeps the clean name and which one gets the suffix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other two name controls
&lt;/h2&gt;

&lt;p&gt;Two more controls sit in step 02, and both compose with the prefix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Find in filename&lt;/strong&gt; / &lt;strong&gt;Replace with&lt;/strong&gt; — a search-and-replace applied to the name, not to the contents. Fill them in and the preview updates as you go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case conversion&lt;/strong&gt; — a control that opens on "Keep as-is".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The preview is the only place the combined result shows, so it is worth reading every line of it before you pack anything. Two rules that each look fine on their own can produce a name you did not want.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get back
&lt;/h2&gt;

&lt;p&gt;The action button reads "Create renamed copies ZIP", and the panel explains the promise: "Create renamed copies ZIP packs the new names into renamed-copies.zip. Names on your device never change."&lt;/p&gt;

&lt;p&gt;One ZIP with the new names inside, and the originals still exactly where they were. Nothing was renamed in place, so if the preview is wrong you have changed nothing and there is nothing to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;If the job is "rename these 300 screenshots by date and number", this tab is quicker than a shell script and it needs no terminal. If the job is "rename the folder itself" or "rename in place", it will not do that — the tool deliberately makes copies. And if the renamed files need to go somewhere else, the ZIP is the hand-off, because there is no upload step and no server step.&lt;/p&gt;

&lt;p&gt;The rest of the File Studio sits next to it: ZIP files, Unzip files, File hash, Find duplicates, Split file and Join parts.&lt;/p&gt;

&lt;p&gt;Disclosure again: I work on OnePassApps, the team behind app.onepassapps.com.&lt;/p&gt;

</description>
      <category>file</category>
      <category>tools</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Merge Two CSVs in the Browser: Rows Align by Header Name, Not Column Order</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 01:27:02 +0000</pubDate>
      <link>https://dev.to/onepassapps/merge-two-csvs-in-the-browser-rows-align-by-header-name-not-column-order-83c</link>
      <guid>https://dev.to/onepassapps/merge-two-csvs-in-the-browser-rows-align-by-header-name-not-column-order-83c</guid>
      <description>&lt;p&gt;Two exports, the same information, slightly different columns, and a tool that wants you to fix the headers by hand before it will combine anything. That last part is usually where I stop using online merge tools.&lt;/p&gt;

&lt;p&gt;The Data Studio's &lt;strong&gt;Merge&lt;/strong&gt; tab in OnePassApps does the matching for you, in the browser tab. The files are read, joined and handed back to you on the same machine — the panel says so plainly: "Nothing leaves this tab." and "Processing stays in this browser."&lt;/p&gt;

&lt;p&gt;Open the &lt;a href="https://app.onepassapps.com/studios/data?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=merge-csv-launch&amp;amp;utm_content=data-studio-link" rel="noopener noreferrer"&gt;Data Studio&lt;/a&gt; to try it, or read the &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=merge-csv-launch&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;app guides&lt;/a&gt; for the rest of the tools.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on OnePassApps.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the merge is laid out
&lt;/h2&gt;

&lt;p&gt;The tab is four numbered steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;01 Source&lt;/strong&gt; — the first file, in a field called "First CSV input"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02 Second CSV&lt;/strong&gt; — the second file, in "Second CSV input"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03 Options&lt;/strong&gt; — a "Delimiter" picker and the rule that governs the whole tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;04 Result&lt;/strong&gt; — "Rows, columns and any parsing issues appear here after the tool runs."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule is the part worth reading twice: "Both files need the same unique headers. Column order may differ; rows are aligned by header name."&lt;/p&gt;

&lt;p&gt;That second sentence is the useful one. Your two files do not have to list their columns in the same order.&lt;/p&gt;

&lt;p&gt;The action button is "Merge compatible CSVs". The delimiter picker defaults to "Auto detect" and offers Comma, Tab, Semicolon and Pipe; on comma-separated input it reported "Delimiter found Comma".&lt;/p&gt;

&lt;h2&gt;
  
  
  A run where the columns are in a different order
&lt;/h2&gt;

&lt;p&gt;First file — name, role, city:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name,role,city
Ada,engineer,Berlin
Grace,engineer,London
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second file — city, name, role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;city,name,role
Helsinki,Linus,maintainer
Berlin,Ada,engineer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After "Merge compatible CSVs" the result panel read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Merged 2 + 2 compatible rows."&lt;/li&gt;
&lt;li&gt;"4 rows · 3 columns · no parsing issues"&lt;/li&gt;
&lt;li&gt;"Delimiter found Comma"&lt;/li&gt;
&lt;li&gt;"Merged in 447 ms"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The preview listed the four merged rows with a detected type on every column — name TEXT, role TEXT, city TEXT — and the exported file kept the &lt;strong&gt;first&lt;/strong&gt; file's column order: name, role, city. The downloaded file was a plain 136-byte CSV with every value quoted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"name","role","city"
"Ada","engineer","Berlin"
"Grace","engineer","London"
"Linus","maintainer","Helsinki"
"Ada","engineer","Berlin"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second file's city/name/role order was resolved by name, and the output follows the first file's header order. So a column-order difference is not a reason to reach for a spreadsheet first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the headers do not match
&lt;/h2&gt;

&lt;p&gt;Now the interesting part, because "merge two CSVs" is usually a request and sometimes a trap. Rename one header in the second file — city becomes country — and the run stops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Could not use this data"&lt;/li&gt;
&lt;li&gt;"CSV schemas do not match."&lt;/li&gt;
&lt;li&gt;"Second CSV is missing: city."&lt;/li&gt;
&lt;li&gt;"Second CSV has extra: country."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The message names both sides of the difference: the header the second file should have had, and the one it brought instead. The preview area says "Nothing to show yet" and "Fix the problem shown above the run action, then run the tool again."&lt;/p&gt;

&lt;p&gt;Now try the other direction — keep all three headers and add a fourth, id — and the run is refused as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"CSV schemas do not match."&lt;/li&gt;
&lt;li&gt;"Second CSV has extra: id."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second case is the important one. Every other header matched, and the tool still refused the whole run instead of merging the three columns it could have paired and quietly dropping the fourth. If you wanted a partial join, this is not it; if you wanted to know exactly which column is in the way, this is exactly it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It stacks, it does not de-duplicate
&lt;/h2&gt;

&lt;p&gt;One more detail you will want before you rely on the output. The merge is a concatenation, not a cleanup. In the run above, "Ada" appears twice in the merged file — once from each input — and the output really does contain four rows for two files of two rows each. "Merged 2 + 2 compatible rows." is literal.&lt;/p&gt;

&lt;p&gt;If you also need the duplicate rows gone, that is a different tab in the same studio, and it reports how many rows it removed instead of folding them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, stated plainly
&lt;/h2&gt;

&lt;p&gt;The studio's source area takes "Drop a data file, or browse CSV, TSV, JSON or YAML · up to 20 MiB · or paste rows in Source". The merge tool itself is a CSV tool: its two fields are described as "Paste the first CSV or drop a file…" and "Paste a CSV with the same headers…", and the button reads "Merge compatible CSVs".&lt;/p&gt;

&lt;p&gt;The plan line on the page reads "FREE PROMOTIONAL PERIOD", "Standard is free right now", "$0 / month", "100,000 credits every 30 days · No end date set · No card required", and "No automatic paid subscription. Existing paid subscriptions keep their terms."&lt;/p&gt;

&lt;h2&gt;
  
  
  One tab of eleven
&lt;/h2&gt;

&lt;p&gt;The Data Studio carries eleven tools on the same page: CSV Viewer, Sort / filter, Columns, Duplicates, Merge, Split, To JSON, JSON Formatter, Validator, To CSV and YAML. They all read the same kind of local data and none of them sends it anywhere, which makes them usable together — sort and filter first, merge two exports second, drop the duplicate rows third.&lt;/p&gt;

&lt;p&gt;If the CSV you are staring at is sensitive, that is the part worth caring about: the work happens in the tab you already have open, and the file never travels to a server that has to be trusted with it.&lt;/p&gt;

</description>
      <category>csv</category>
      <category>data</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Extract Audio From a Video in the Browser: WAV or WebM Opus, Checked Byte by Byte</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 01:09:33 +0000</pubDate>
      <link>https://dev.to/onepassapps/extract-audio-from-a-video-in-the-browser-wav-or-webm-opus-checked-byte-by-byte-4m95</link>
      <guid>https://dev.to/onepassapps/extract-audio-from-a-video-in-the-browser-wav-or-webm-opus-checked-byte-by-byte-4m95</guid>
      <description>&lt;p&gt;Pulling the sound out of a video usually means uploading the file to somebody's server. There is a&lt;br&gt;
second way: decode the audio track in the tab you already have open, and write the result to your&lt;br&gt;
own disk.&lt;/p&gt;

&lt;p&gt;I built a small MP4 to test this with - a six second clip with a 440 Hz tone in it - and ran it&lt;br&gt;
through the Extract tab of the Audio Studio at &lt;a href="https://app.onepassapps.com/studios/audio?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=audio-extract-format&amp;amp;utm_content=audio-studio-link" rel="noopener noreferrer"&gt;app.onepassapps.com/studios/audio&lt;/a&gt;.&lt;br&gt;
I work on OnePassApps, so this is our tool and our test.&lt;/p&gt;

&lt;p&gt;The headline: the file never leaves the tab, and you get a real audio file, not a preview.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the panel actually offers
&lt;/h2&gt;

&lt;p&gt;The tab is called &lt;strong&gt;Extract&lt;/strong&gt;, and it has three steps. Before you load anything, step 01 reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No video yet. Drop one on the stage or browse for it. Up to 32 MiB and 45 minutes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Step 02 is where the interesting choices live, and they only appear once a video has been checked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Format, sample rate and channels appear here once the video has been checked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The page itself sets the boundary - the file hint reads "MP4, MOV, MKV, WebM or MPEG-TS with an&lt;br&gt;
audio track · up to 32 MiB and 45 minutes" - and the page line says "Convert, compress, trim and&lt;br&gt;
extract audio locally. Your media stays in this browser tab."&lt;/p&gt;

&lt;p&gt;Load a video and three dropdowns appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output format&lt;/strong&gt;: &lt;code&gt;WAV audio (PCM)&lt;/code&gt; or &lt;code&gt;WebM audio (Opus)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample rate&lt;/strong&gt;: &lt;code&gt;48,000 Hz&lt;/code&gt;, &lt;code&gt;44,100 Hz&lt;/code&gt;, &lt;code&gt;32,000 Hz&lt;/code&gt;, &lt;code&gt;24,000 Hz&lt;/code&gt;, &lt;code&gt;16,000 Hz&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channels&lt;/strong&gt;: for a mono source, &lt;code&gt;Mono&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a note under them: "Defaults keep the source’s mono and sample rate when the chosen&lt;br&gt;
encoder supports them." In practice that means the defaults followed my file - a 44.1 kHz mono&lt;br&gt;
source came back as &lt;code&gt;44,100 Hz&lt;/code&gt; and &lt;code&gt;Mono&lt;/code&gt; without me touching anything.&lt;/p&gt;

&lt;p&gt;One line in step 01 tells you what you are about to get, and it matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only the primary audio track is exported. Video and other tracks are discarded locally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So this is an audio extraction, not a repackage of the whole file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run 1: the defaults
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;probe-extract.mp4&lt;/code&gt; on the stage, the panel showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MP4 · 0:06.0 · 98.7 KiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the stage header confirmed what was inside it before I touched anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container     MP4
Audio codec   AAC
Sample rate   44,100 Hz
Channels      Mono
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pressing &lt;strong&gt;Extract audio&lt;/strong&gt; had produced the file within three seconds and the result panel read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OUTPUT
probe-extract-audio.wav
0:06.0 · 519 KiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;Format WAV&lt;/code&gt;, &lt;code&gt;Sample rate 44,100 Hz&lt;/code&gt;, &lt;code&gt;Channels Mono&lt;/code&gt;, &lt;code&gt;Size 519 KiB&lt;/code&gt;, and the line that&lt;br&gt;
tells you nothing went anywhere else:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Output created locally. Your original file remains unchanged.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The output file name is derived from the source - &lt;code&gt;probe-extract.mp4&lt;/code&gt; became&lt;br&gt;
&lt;code&gt;probe-extract-audio.wav&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The plan block on the page reads "Standard is free right now" and "$0 / month" with "No card required", and the run above was done in a tab that was not signed in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read the bytes back, do not trust the label
&lt;/h2&gt;

&lt;p&gt;A panel that says "WAV" and a panel that says "MP3" are both just text. So I pulled the actual&lt;br&gt;
downloaded blob off the page and parsed it with &lt;code&gt;ffprobe&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;codec_name&lt;/span&gt;=&lt;span class="n"&gt;pcm_s16le&lt;/span&gt;
&lt;span class="n"&gt;sample_rate&lt;/span&gt;=&lt;span class="m"&gt;44100&lt;/span&gt;
&lt;span class="n"&gt;channels&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;duration&lt;/span&gt;=&lt;span class="m"&gt;6&lt;/span&gt;.&lt;span class="m"&gt;023220&lt;/span&gt;
&lt;span class="n"&gt;format_name&lt;/span&gt;=&lt;span class="n"&gt;wav&lt;/span&gt;
&lt;span class="n"&gt;size&lt;/span&gt;=&lt;span class="m"&gt;531292&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a genuine RIFF/WAVE file - the first bytes are &lt;code&gt;RIFF&lt;/code&gt; then &lt;code&gt;WAVE&lt;/code&gt; then &lt;code&gt;fmt&lt;/code&gt; - with&lt;br&gt;
16-bit signed little-endian PCM, one channel, at the rate the source had.&lt;/p&gt;

&lt;p&gt;The better test is the content. My test video carried a 440 Hz sine tone, so if the extractor really&lt;br&gt;
decoded the track, the WAV should contain a 440 Hz waveform. Counting zero crossings over one&lt;br&gt;
second of the extracted audio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frames 265624  channels 1  sampwidth 2  rate 44100  dur 6.023
peak 6204  rms 2886.6
zero crossings in 1s: 880  -&amp;gt; approx freq 440 Hz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;880 crossings per second is 440 Hz, exactly the tone I encoded. The audio in the file is the audio&lt;br&gt;
from the video.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run 2: switch the format and the rate
&lt;/h2&gt;

&lt;p&gt;The format picker is a real dropdown with real choices, so I changed two of them - &lt;code&gt;WebM audio&lt;br&gt;
(Opus)&lt;/code&gt; and &lt;code&gt;48,000 Hz&lt;/code&gt; - and ran it again. The options list for the rate picker was&lt;br&gt;
&lt;code&gt;48,000 Hz&lt;/code&gt;, &lt;code&gt;44,100 Hz&lt;/code&gt;, &lt;code&gt;32,000 Hz&lt;/code&gt;, &lt;code&gt;24,000 Hz&lt;/code&gt;, &lt;code&gt;16,000 Hz&lt;/code&gt;, and the channels list for this&lt;br&gt;
mono source held a single entry, &lt;code&gt;Mono&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second run produced a different file and a different container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OUTPUT
probe-extract-audio.webm
0:06.0 · 98.1 KiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;Format WebM Opus&lt;/code&gt;, &lt;code&gt;Sample rate 48,000 Hz&lt;/code&gt;, &lt;code&gt;Channels Mono&lt;/code&gt;, &lt;code&gt;Size 98.1 KiB&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ffprobe&lt;/code&gt; on that blob:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;codec_name&lt;/span&gt;=&lt;span class="n"&gt;opus&lt;/span&gt;
&lt;span class="n"&gt;sample_rate&lt;/span&gt;=&lt;span class="m"&gt;48000&lt;/span&gt;
&lt;span class="n"&gt;channels&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;format_name&lt;/span&gt;=&lt;span class="n"&gt;matroska&lt;/span&gt;,&lt;span class="n"&gt;webm&lt;/span&gt;
&lt;span class="n"&gt;duration&lt;/span&gt;=&lt;span class="m"&gt;6&lt;/span&gt;.&lt;span class="m"&gt;040000&lt;/span&gt;
&lt;span class="n"&gt;size&lt;/span&gt;=&lt;span class="m"&gt;100485&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the file starts with the four-byte WebM magic &lt;code&gt;1a 45 df a3&lt;/code&gt;. So the picker is not decoration -&lt;br&gt;
it changes the container, the codec and the sample rate of the file you download.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;Channels&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;ffprobe says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;WAV audio (PCM)&lt;/td&gt;
&lt;td&gt;44,100 Hz&lt;/td&gt;
&lt;td&gt;Mono&lt;/td&gt;
&lt;td&gt;519 KiB&lt;/td&gt;
&lt;td&gt;pcm_s16le, 44100 Hz, 1 channel, 6.023 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;WebM audio (Opus)&lt;/td&gt;
&lt;td&gt;48,000 Hz&lt;/td&gt;
&lt;td&gt;Mono&lt;/td&gt;
&lt;td&gt;98.1 KiB&lt;/td&gt;
&lt;td&gt;opus, 48000 Hz, 1 channel, 6.040 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly a fifth of the size for the second run. Neither file is smaller than it needs to be -&lt;br&gt;
uncompressed WAV is uncompressed WAV - but if you are moving audio over a chat window or attaching&lt;br&gt;
it to something, 98 KiB beats 519 KiB, and Opus is the format most players take without asking&lt;br&gt;
questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest edges
&lt;/h2&gt;

&lt;p&gt;A few things this test does &lt;strong&gt;not&lt;/strong&gt; cover, so you know what you are reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I tested a &lt;strong&gt;mono&lt;/strong&gt; source. The channels picker offered only &lt;code&gt;Mono&lt;/code&gt; for it. A stereo source
should offer &lt;code&gt;Stereo&lt;/code&gt;, but I did not test one, so I am not claiming what the list looks like
then.&lt;/li&gt;
&lt;li&gt;I tested &lt;strong&gt;one&lt;/strong&gt; container, MP4. MOV, MKV, WebM and MPEG-TS are accepted by the file hint, but
only the MP4 was proved.&lt;/li&gt;
&lt;li&gt;The extracted WAV is &lt;strong&gt;6.023 s&lt;/strong&gt; against a &lt;strong&gt;6.000 s&lt;/strong&gt; source. The extra couple of hundredths
are decoder padding, not a different edit.&lt;/li&gt;
&lt;li&gt;The result panel is a summary. The bytes on disk are the evidence, and you can check them the
same way I did.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When it is worth it
&lt;/h2&gt;

&lt;p&gt;The case for doing this in the tab is simple: the file is on your machine already, and you want a&lt;br&gt;
smaller file that is still the same audio. A recording you need as a voice note. A clip from a&lt;br&gt;
screencast you want to send to someone. A track you want to transcribe.&lt;/p&gt;

&lt;p&gt;If you want the walkthrough with screenshots and the button-by-button version, the&lt;br&gt;
&lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=audio-extract-format&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps guides&lt;/a&gt;&lt;br&gt;
have it.&lt;/p&gt;

&lt;p&gt;I work on OnePassApps - we build the browser studios behind app.onepassapps.com, where the&lt;br&gt;
processing happens in your tab and the file stays with you.&lt;/p&gt;

</description>
      <category>audio</category>
      <category>webdev</category>
      <category>tools</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Resize an Image in the Browser: Three Numbers That Move Together</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Tue, 03 Nov 2026 00:47:50 +0000</pubDate>
      <link>https://dev.to/onepassapps/resize-an-image-in-the-browser-three-numbers-that-move-together-3dja</link>
      <guid>https://dev.to/onepassapps/resize-an-image-in-the-browser-three-numbers-that-move-together-3dja</guid>
      <description>&lt;h2&gt;
  
  
  Resizing is three numbers, and only one of them is yours to set
&lt;/h2&gt;

&lt;p&gt;Most resize tools give you a slider or a handful of presets. That is fine until you need an exact pixel size for a form, a sprite sheet or a CMS that rejects anything else - at which point you end up guessing and checking the result by hand.&lt;/p&gt;

&lt;p&gt;The Image Studio's &lt;strong&gt;Resize&lt;/strong&gt; tab in OnePassApps takes the opposite approach: it shows you the width, the height and the scale of your image as live numbers, tells you which of them are locked, and hands you the resized file so you can check it yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full disclosure: I work on OnePassApps.&lt;/strong&gt; This is the Resize tab in the Image Studio, checked in the live tab while writing this - every number below came out of a run I made, not out of the marketing copy.&lt;/p&gt;

&lt;p&gt;Open it here: &lt;a href="https://app.onepassapps.com/studios/image?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=image-resize-dimensions&amp;amp;utm_content=image-studio-link" rel="noopener noreferrer"&gt;the Image Studio&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel
&lt;/h2&gt;

&lt;p&gt;The Image Studio carries seven tool tabs on one page: &lt;strong&gt;Convert, Compress, Resize, Crop, Remove background, Remove metadata, Images to PDF&lt;/strong&gt;. The page line above them reads "Convert, compress, resize, crop and clean images in your browser. Nothing leaves this tab."&lt;/p&gt;

&lt;p&gt;The Resize tab is laid out as three numbered steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;01 Source&lt;/strong&gt; - "Add an image to start. Your original stays untouched." The picker accepts JPEG, PNG or WebP · up to 25 MiB and 25 megapixels, and the page repeats "Processing stays in this browser."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;02 Dimensions&lt;/strong&gt; - "Width, height and scale appear here once an image is open. They start at its real size." Three number fields - &lt;strong&gt;Width (px)&lt;/strong&gt;, &lt;strong&gt;Height (px)&lt;/strong&gt; and &lt;strong&gt;Scale (%)&lt;/strong&gt; - then two checkboxes, &lt;strong&gt;Lock aspect&lt;/strong&gt; and &lt;strong&gt;Prevent upscaling&lt;/strong&gt;, a &lt;strong&gt;Format&lt;/strong&gt; row of PNG, JPEG and WebP, a &lt;strong&gt;Quality&lt;/strong&gt; control, and the line "Resampled in this browser, up to 25 megapixels."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;03 Result&lt;/strong&gt; - "The new copy's size and format appear here."&lt;/p&gt;

&lt;p&gt;The action button is &lt;strong&gt;Resize image&lt;/strong&gt;, and after a run it becomes "Resize again". The panel also carries a "Fit" / "100%" preview zoom and a &lt;strong&gt;Result&lt;/strong&gt; / &lt;strong&gt;Original&lt;/strong&gt; toggle so you can compare the two side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three numbers, and which one you actually touch
&lt;/h2&gt;

&lt;p&gt;Open a 400 × 300 image and the fields read 400, 300 and 100. Type 100 into the width and the height does not stay at 300 - it becomes 75, and the scale drops to 25. That is the &lt;strong&gt;Lock aspect&lt;/strong&gt; checkbox doing its job: it is on by default, and it keeps the ratio of your image while you resize it.&lt;/p&gt;

&lt;p&gt;Set the scale instead and the other two follow. Scale 50 on the same 400 × 300 image gave 200 × 150 automatically.&lt;/p&gt;

&lt;p&gt;So with the lock on, you type one number and the panel does the arithmetic. The fields are not three separate settings; they are three views of the same decision.&lt;/p&gt;

&lt;p&gt;The limits are visible in the markup rather than buried: width and height run from 1 to 12000, the scale field from 1 to 400, and the quality control from 10 to 100, starting at 86.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checkbox that stops you making the file worse
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prevent upscaling&lt;/strong&gt; is also on by default, and it is the one that surprises people. With it on, typing a width larger than the source is refused: on the 400 × 300 image, entering 800 left the field reading 400, with the height back at 300 and the scale back at 100.&lt;/p&gt;

&lt;p&gt;Turn it off and the same 800 is accepted. The width field became 800, the height followed to 600, and the scale showed 200.&lt;/p&gt;

&lt;p&gt;That is a deliberate guard rather than a limit on the format. Enlarging a 400 × 300 image to 800 × 600 invents 240,000 pixels that were never in the file - they are filled in by interpolation, so you get a bigger file with less real detail in it. The checkbox is there so that "make it bigger" is something you ask for on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving the output instead of trusting the panel
&lt;/h2&gt;

&lt;p&gt;Here is the run, in full, on a 400 × 300 PNG I generated myself (four flat colour quadrants with a black marker square in the top-left corner).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 1 - downscale to 100 px wide.&lt;/strong&gt; Width 100, lock aspect on, default JPEG. The panel reported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Before 1.8KiB PNG"&lt;/li&gt;
&lt;li&gt;"After 1.3KiB JPEG"&lt;/li&gt;
&lt;li&gt;"Dimensions 400 × 300 → 100 × 75"&lt;/li&gt;
&lt;li&gt;"File name probe-resize.jpg"&lt;/li&gt;
&lt;li&gt;"Resized in 0.2 s"&lt;/li&gt;
&lt;li&gt;"JPEG · 100 × 75 · 1.3 KiB · 24% smaller"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exported file was a real JPEG - 1,378 bytes with a JFIF header. Reading its pixels back, the top-left pixel was black (0,0,0,255), the top-right green (0,255,0,255), the bottom-left blue (0,0,255,255) and the bottom-right white (255,255,255,255): the same four quadrants, resampled into the smaller frame, in the same places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 2 - upscale to 800 px wide.&lt;/strong&gt; Prevent upscaling off, width 800. The panel reported "Dimensions 400 × 300 → 800 × 600", "After 6.9KiB JPEG" and "JPEG · 800 × 600 · 6.9 KiB · 290% larger". The exported file was 7,064 bytes of JPEG, and the four corner pixels still read black, green, blue and white.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 3 - PNG output at 120 px wide.&lt;/strong&gt; Format PNG, width 120, height followed to 90, scale 30. The panel reported "After 618B PNG", "Dimensions 400 × 300 → 120 × 90", "PNG · 120 × 90 · 618 B · 66% smaller". The exported file's IHDR header read 120 × 90, and its corner pixels read black, green, blue and white again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default output format is JPEG, and the size line can read "larger"
&lt;/h2&gt;

&lt;p&gt;The Format row defaults to JPEG, so the first resize I made came out as "probe-resize.jpg" even though the source was a PNG.&lt;/p&gt;

&lt;p&gt;Read the size line before you take it. Upscaling that flat-colour PNG to 800 × 600 produced "6.9 KiB · 290% larger" - the file got three and a half times the size of the original, because JPEG re-encodes what PNG had stored losslessly and the result is still only 400 × 300 of real pixels. The panel is not hiding it: it shows the before and after sizes, the percentage, and a Result / Original toggle so you can compare the two.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;PNG&lt;/strong&gt; when you want a lossless result. The same downscale to 120 × 90 came out at 618 bytes, against 1.8 KiB for the source - 66% smaller, and every pixel in it still exactly matches the colour it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get at the end
&lt;/h2&gt;

&lt;p&gt;The result panel gives you the file name, the format, the pixel dimensions, the size change, the time it took ("Resized in 0.2 s"), and a &lt;strong&gt;Download&lt;/strong&gt; button. The closing line is the honest one: "Finished. Download a new copy; the original stays unchanged." Your source file is never overwritten, and you keep both copies side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do it in the tab at all
&lt;/h2&gt;

&lt;p&gt;The usual reason to resize something is that it goes somewhere you do not control - an upload box that wants exactly 800 × 600, a form with a size limit, an avatar field that wants a square. Those are exactly the files you would rather not hand to a stranger's server, and this tab never sends them: the page says "Nothing leaves this tab." and "Processing stays in this browser.", and the only thing that leaves the browser is the file you download yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full disclosure again: I work on OnePassApps.&lt;/strong&gt; The Image Studio is free right now during the promotional period - Standard is $0/month with 100,000 credits every 30 days, no end date set and no card required, and existing paid subscriptions keep their terms. If you want the other tabs in the same place, the guides are here: &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=image-resize-dimensions&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;OnePassApps app guides&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>image</category>
      <category>tools</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
