openvibers/openvibe.community Commits
A Markdown paste's opening heading is left out when it only repeats the title (the page's h1 shows it) (#148)
Changes
@@@ -347,6 +347,16 @@ function shareLinks(p) {* comments: the paste's typed thread as the comment service shows it to this viewer (null while nobody has* commented); commentSort 'new' or 'old', commentsAfter the paging cursor. No comments on burn-after-read pastes.*/++/** Letters and digits only, lower case: a heading and a title match whatever their punctuation and emoji. */++const bare = (t) => String(t || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');++++/** A document's opening # heading, dropped when it only repeats the paste's title (the page's h1 already shows it). */++function withoutTitle(content, title) {++ const text = String(content || '');++ const m = text.match(/^\s*# +([^\n]+)\n?/);++ return m && bare(m[1]) && bare(m[1]) === bare(title) ? text.slice(m[0].length) : text;++}++function pastePage({ paste: p, related, user, comments = null, commentSort = 'new', commentsAfter = null }) {const isShot = p.type === 'screenshot';const title = p.title || (isShot ? 'Screenshot' : 'Untitled paste');@@@ -377,7 +387,7 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne</div>`;// A Markdown paste is a document: shown formatted (the same safe renderer as forum posts), its source a click// away. Copy, Raw and Download still give the source.-- bodyBlock = isDoc ? `<div class="md paste-doc">${renderMarkdown(p.content, { headingShift: 1, document: true })}</div>++ bodyBlock = isDoc ? `<div class="md paste-doc">${renderMarkdown(withoutTitle(p.content, title), { headingShift: 1, document: true })}</div><details class="paste-source"><summary><i class="fa-solid fa-code" aria-hidden="true"></i> Markdown source <span class="muted small">${num(hl.lines)} lines</span></summary>${code}@@@ -94,10 +94,13 @@ const { boot, check, done } = require('./helpers/app');});await check('a Markdown paste reads as a document, its source a click away; code pastes are unchanged', async () => {-- const doc = (await svc.createText(anon, { title: 'Notes', content: '# Notes\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' })).slug;++ const doc = (await svc.createText(anon, { title: 'Notes', content: '# Notes!\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' })).slug;const r = await t.get(`/p/${doc}`);assert.strictEqual(r.status, 200);-- has(r.text, '<div class="md paste-doc"><h2>Notes</h2>\n<h3>Part</h3>');++ // Its opening # only repeats the title (the page's h1), so it is left out and ### leads at h2.++ has(r.text, '<div class="md paste-doc"><h2>Part</h2>');++ const other = (await svc.createText(anon, { title: 'Release', content: '# Something else\n\ntext', language: 'markdown' })).slug;++ has((await t.get(`/p/${other}`)).text, '<div class="md paste-doc"><h2>Something else</h2>');has(r.text, '<td class="al-r">2</td>');has(r.text, '<li>x<ul><li>y</li></ul></li>');has(r.text, '<details class="paste-source">');
@@ -347,6 +347,16 @@ function shareLinks(p) {
@@ -347,6 +347,16 @@ function shareLinks(p) {
* comments: the paste's typed thread as the comment service shows it to this viewer (null while nobody has
* comments: the paste's typed thread as the comment service shows it to this viewer (null while nobody has
* commented); commentSort 'new' or 'old', commentsAfter the paging cursor. No comments on burn-after-read pastes.
* commented); commentSort 'new' or 'old', commentsAfter the paging cursor. No comments on burn-after-read pastes.
*/
*/
+/** Letters and digits only, lower case: a heading and a title match whatever their punctuation and emoji. */
+const bare = (t) => String(t || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
+
+/** A document's opening # heading, dropped when it only repeats the paste's title (the page's h1 already shows it). */
+function withoutTitle(content, title) {
+ const text = String(content || '');
+ const m = text.match(/^\s*# +([^\n]+)\n?/);
+ return m && bare(m[1]) && bare(m[1]) === bare(title) ? text.slice(m[0].length) : text;
+}
+
function pastePage({ paste: p, related, user, comments = null, commentSort = 'new', commentsAfter = null }) {
function pastePage({ paste: p, related, user, comments = null, commentSort = 'new', commentsAfter = null }) {
const isShot = p.type === 'screenshot';
const isShot = p.type === 'screenshot';
const title = p.title || (isShot ? 'Screenshot' : 'Untitled paste');
const title = p.title || (isShot ? 'Screenshot' : 'Untitled paste');
@@ -377,7 +387,7 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne
@@ -377,7 +387,7 @@ function pastePage({ paste: p, related, user, comments = null, commentSort = 'ne
</div>`;
</div>`;
// A Markdown paste is a document: shown formatted (the same safe renderer as forum posts), its source a click
// A Markdown paste is a document: shown formatted (the same safe renderer as forum posts), its source a click
// away. Copy, Raw and Download still give the source.
// away. Copy, Raw and Download still give the source.
- bodyBlock = isDoc ? `<div class="md paste-doc">${renderMarkdown(p.content, { headingShift: 1, document: true })}</div>
+ bodyBlock = isDoc ? `<div class="md paste-doc">${renderMarkdown(withoutTitle(p.content, title), { headingShift: 1, document: true })}</div>
<details class="paste-source">
<details class="paste-source">
<summary><i class="fa-solid fa-code" aria-hidden="true"></i> Markdown source <span class="muted small">${num(hl.lines)} lines</span></summary>
<summary><i class="fa-solid fa-code" aria-hidden="true"></i> Markdown source <span class="muted small">${num(hl.lines)} lines</span></summary>
${code}
${code}
@@ -94,10 +94,13 @@ const { boot, check, done } = require('./helpers/app');
@@ -94,10 +94,13 @@ const { boot, check, done } = require('./helpers/app');
});
});
await check('a Markdown paste reads as a document, its source a click away; code pastes are unchanged', async () => {
await check('a Markdown paste reads as a document, its source a click away; code pastes are unchanged', async () => {
- const doc = (await svc.createText(anon, { title: 'Notes', content: '# Notes\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' })).slug;
+ const doc = (await svc.createText(anon, { title: 'Notes', content: '# Notes!\n\n### Part\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n- x\n - y\n\n<script>alert(1)</script>', language: 'markdown' })).slug;
const r = await t.get(`/p/${doc}`);
const r = await t.get(`/p/${doc}`);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- has(r.text, '<div class="md paste-doc"><h2>Notes</h2>\n<h3>Part</h3>');
+ // Its opening # only repeats the title (the page's h1), so it is left out and ### leads at h2.
+ has(r.text, '<div class="md paste-doc"><h2>Part</h2>');
+ const other = (await svc.createText(anon, { title: 'Release', content: '# Something else\n\ntext', language: 'markdown' })).slug;
+ has((await t.get(`/p/${other}`)).text, '<div class="md paste-doc"><h2>Something else</h2>');
has(r.text, '<td class="al-r">2</td>');
has(r.text, '<td class="al-r">2</td>');
has(r.text, '<li>x<ul><li>y</li></ul></li>');
has(r.text, '<li>x<ul><li>y</li></ul></li>');
has(r.text, '<details class="paste-source">');
has(r.text, '<details class="paste-source">');