@dunkinfrunkin/mdcat 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunkinfrunkin/mdcat",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "View markdown files beautifully in your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync } from "fs";
3
3
  import { resolve, basename } from "path";
4
4
  import { tmpdir } from "os";
5
5
  import { execFileSync } from "child_process";
6
- import { marked } from "marked";
6
+ import { marked, Marked } from "marked";
7
7
  import { renderTokens } from "./render.js";
8
8
  import { launch } from "./tui.js";
9
9
  import { toDocx } from "./docx.js";
@@ -45,14 +45,28 @@ if (args[0] === "--version" || args[0] === "-v") {
45
45
 
46
46
  const MAX_COLS = 100;
47
47
 
48
+ function escapeHtml(s) {
49
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
50
+ }
51
+
48
52
  function openInBrowser(title, content) {
49
- const html = marked.parse(content);
53
+ // Use a custom renderer that escapes raw HTML tokens so bare <tag> in
54
+ // markdown text isn't swallowed by the browser.
55
+ const webMarked = new Marked({ gfm: true });
56
+ webMarked.use({
57
+ renderer: {
58
+ html(token) {
59
+ return escapeHtml(token.text);
60
+ },
61
+ },
62
+ });
63
+ const html = webMarked.parse(content);
50
64
  const page = `<!DOCTYPE html>
51
65
  <html lang="en">
52
66
  <head>
53
67
  <meta charset="UTF-8">
54
68
  <meta name="viewport" content="width=device-width, initial-scale=1">
55
- <title>${title}</title>
69
+ <title>${escapeHtml(title)}</title>
56
70
  <style>
57
71
  * { box-sizing: border-box; margin: 0; padding: 0; }
58
72
  body {
@@ -98,9 +112,10 @@ function openInBrowser(title, content) {
98
112
  }
99
113
 
100
114
  function runTUI(title, content) {
101
- const cols = Math.min(process.stdout.columns || 80, MAX_COLS);
115
+ const termCols = process.stdout.columns || 80;
116
+ const cols = Math.min(termCols, MAX_COLS);
102
117
  const tokens = marked.lexer(content);
103
- const lines = renderTokens(tokens, cols);
118
+ const lines = renderTokens(tokens, cols, termCols > MAX_COLS ? termCols : undefined);
104
119
  launch(title, lines);
105
120
  }
106
121
 
package/src/docx.js CHANGED
@@ -104,7 +104,9 @@ function htmlDecode(s) {
104
104
  .replace(/&gt;/g, ">")
105
105
  .replace(/&quot;/g, '"')
106
106
  .replace(/&#39;/g, "'")
107
- .replace(/&nbsp;/g, " ");
107
+ .replace(/&nbsp;/g, " ")
108
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
109
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
108
110
  }
109
111
 
110
112
  // ─── Block token → Paragraph[] ──────────────────────────────────────────────
package/src/render.js CHANGED
@@ -95,7 +95,9 @@ function htmlDecode(s) {
95
95
  .replace(/&gt;/g, ">")
96
96
  .replace(/&quot;/g, '"')
97
97
  .replace(/&#39;/g, "'")
98
- .replace(/&nbsp;/g, " ");
98
+ .replace(/&nbsp;/g, " ")
99
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
100
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
99
101
  }
100
102
 
101
103
  // ─── Inline renderer ───────────────────────────────────────────────────────────
@@ -202,11 +204,6 @@ function code(tok, w) {
202
204
  // Expand tabs to 2 spaces
203
205
  const rawText = (tok.text ?? "").replace(/\t/g, " ");
204
206
 
205
- // innerW = content width between borders (1 space padding each side)
206
- const innerW = w - 2;
207
- // borderW = total inner span for the box (includes the 1px padding each side)
208
- const borderW = innerW + 2;
209
-
210
207
  let rawLines;
211
208
  if (rawText === "") {
212
209
  rawLines = [""];
@@ -239,13 +236,21 @@ function code(tok, w) {
239
236
  // Pad to same length just in case highlight produced different line count
240
237
  while (highlighted.length < rawLines.length) highlighted.push("");
241
238
 
239
+ // Size the box to the longest line (capped at terminal width)
240
+ const maxLineW = highlighted.reduce((m, l) => Math.max(m, vlen(l)), 0);
241
+ const langTagLen = lang ? 1 + lang.length + 1 : 0; // " lang " visual chars
242
+ // innerW = content area between pipes (1 space padding each side)
243
+ // Must fit: longest line, lang tag in top border, and minimum 4 chars
244
+ const innerW = Math.min(w - 2, Math.max(maxLineW, langTagLen + 2, 4));
245
+ // borderW = total inner span for the box (includes the 1-char padding each side)
246
+ const borderW = innerW + 2;
247
+
242
248
  const lines = [""];
243
249
 
244
250
  // Top border: ┌─ lang ─────┐ or ┌──────────────┐
245
251
  let top;
246
252
  if (lang) {
247
253
  const langTag = " " + c.codeLang(lang) + " ";
248
- const langTagLen = 1 + lang.length + 1; // visual chars
249
254
  const fill = Math.max(0, borderW - langTagLen - 1); // -1 for leading "─"
250
255
  top =
251
256
  c.border("┌─") +
@@ -463,7 +468,7 @@ function hr(w) {
463
468
 
464
469
  // ─── Token dispatcher ──────────────────────────────────────────────────────────
465
470
 
466
- function token(tok, w) {
471
+ function token(tok, w, tableW) {
467
472
  try {
468
473
  switch (tok.type) {
469
474
  case "heading":
@@ -477,7 +482,7 @@ function token(tok, w) {
477
482
  case "list":
478
483
  return ["", ...list(tok, w, 0), ""];
479
484
  case "table":
480
- return table(tok, w);
485
+ return table(tok, tableW ?? w);
481
486
  case "hr":
482
487
  return hr(w);
483
488
  case "space":
@@ -499,9 +504,11 @@ function token(tok, w) {
499
504
  *
500
505
  * @param {import("marked").Token[]} tokens - Output of `marked.lexer()`
501
506
  * @param {number} cols - Terminal width (default 80)
507
+ * @param {number} [fullCols] - Uncapped terminal width for tables
502
508
  * @returns {string[]}
503
509
  */
504
- export function renderTokens(tokens, cols = 80) {
510
+ export function renderTokens(tokens, cols = 80, fullCols) {
505
511
  const w = Math.max(20, cols - MARGIN.length * 2);
506
- return tokens.flatMap((tok) => token(tok, w));
512
+ const tableW = fullCols ? Math.max(20, fullCols - MARGIN.length * 2) : undefined;
513
+ return tokens.flatMap((tok) => token(tok, w, tableW));
507
514
  }