-
Notifications
You must be signed in to change notification settings - Fork 97
Txt2tags it #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
luginf
wants to merge
16
commits into
qownnotes:main
Choose a base branch
from
luginf:txt2tags-it
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Txt2tags it #284
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
de38424
adding txt2tags-it: support to txt2tags syntax in addition to the mar…
8ab43f1
fix: strikethrough, HTML options default, link rules ordering
b33b8be
updating and fixing
edf9e8c
fix version and credits
525f497
fix: Qt6 compatibility — resolve markdown-it constructors via module …
394869d
fix: Qt6 UMD compat — use globalThis when module this is undefined
86145df
fix: Qt6 compat — top-level var export; remove deflist and katex
cb3881c
chore: remove unused deflist and katex plugins
b9c9b0c
Guard URL schemes
pbek af82b50
remove obsolete ressources in info.json
17b3360
fix html quotes
89efeb4
fix comment % after a line break
80a4a97
remove setext md by default (activable via option)
885aa1e
fix setex line
669979b
adding shortcuts for txt2tags syntax (italic, underline, strikes., he…
bc2c443
fixed various copilot inputs in #284
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # txt2tags-it | ||
|
|
||
| This script for QOwnNotes is based on https://github.com/qownnotes/scripts/tree/main/markdown-it | ||
|
|
||
| It allows the use of the txt2tags syntax, in addition to the markdown one, in both the editor and in the preview windows of QOwnNotes. | ||
|
|
||
| Made with the help of some LLM. | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "name": "txt2tags-it", | ||
| "identifier": "txt2tags-it", | ||
| "script": "txt2tags-it.qml", | ||
| "resources": [ | ||
| "markdown-it.js", | ||
| "markdown-it-txt2tags.js" | ||
| ], | ||
| "authors": ["@luginf"], | ||
| "version": "0.1", | ||
| "minAppVersion": "26.4.11", | ||
| "description": "This script, based on markdown-it, replaces the default markdown renderer with markdown-it AND also with the txt2tags syntax.\n\n<b>Dependencies</b>\n<a href=\"https://github.com/markdown-it/markdown-it\">markdown-it.js</a> (v8.4.2 bundled with the script)\n\n<b>Usage</b>\nFor the possible configuration options check <a href=\"https://github.com/markdown-it/markdown-it/tree/main/lib/presets\">here</a>.\n\n<b>Important</b>\nThis script currently only works with <a href=\"https://github.com/qownnotes/scripts/issues/77\"><b>legacy media links</b></a>. You can turn them on in the <i>General Settings</i>.\n\nImportant note: You need to use legacy image linking with this script, otherwise there will be no images shown in the preview!" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,347 @@ | ||
| /*! markdown-it-txt2tags - txt2tags syntax support for markdown-it */ | ||
| // Top-level var makes markdownitTxt2tags accessible as MarkdownItTxt2tags.markdownitTxt2tags in QML (Qt5 + Qt6). | ||
| var markdownitTxt2tags; | ||
| (function (f) { | ||
| if (typeof exports === "object" && typeof module !== "undefined") { | ||
| module.exports = f(); | ||
| } else if (typeof define === "function" && define.amd) { | ||
| define([], f); | ||
| } else { | ||
| markdownitTxt2tags = f(); | ||
| } | ||
| })(function () { | ||
| "use strict"; | ||
|
|
||
| /** | ||
| * txt2tags syntax support for markdown-it | ||
| * | ||
| * Block rules: | ||
| * = Heading 1 = / == Heading 2 == / … / ===== Heading 5 ===== | ||
| * % This is a comment (line ignored in output) | ||
| * | ||
| * Inline rules: | ||
| * //italic// → <em>italic</em> | ||
| * __underline__ → <u>underline</u> | ||
| * --strikethrough-- → <del>strikethrough</del> | ||
| * | ||
| * Options: | ||
| * useSetextHeadings {boolean} — enable markdown setext headings (Title\n===). | ||
| * Default: false. | ||
| */ | ||
| function txt2tagsPlugin(md, options) { | ||
| options = Object.assign({ useSetextHeadings: false }, options); | ||
|
|
||
| if (!options.useSetextHeadings) { | ||
| md.block.ruler.disable("lheading"); | ||
| } | ||
|
|
||
|
|
||
| // ── Override text rule to also stop at / (needed for //italic//) ───────── | ||
| // markdown-it's built-in text rule stops at isTerminatorChar characters. | ||
| // '/' (0x2F) is not in that set, so '//italic//' gets consumed as plain | ||
| // text before the italic inline rule can match it. | ||
| function isTerminatorCharExtended(ch) { | ||
| // 0x2F = '/' — added for txt2tags //italic// | ||
| if (ch === 0x2F) return true; | ||
| // Replicate markdown-it's isTerminatorChar exactly (v8.4.2) | ||
| switch (ch) { | ||
| case 0x0a: case 0x21: case 0x23: case 0x24: case 0x25: case 0x26: | ||
| case 0x2a: case 0x2b: case 0x2d: case 0x3a: case 0x3c: case 0x3d: | ||
| case 0x3e: case 0x40: case 0x5b: case 0x5c: case 0x5d: case 0x5e: | ||
| case 0x5f: case 0x60: case 0x7b: case 0x7d: case 0x7e: | ||
| return true; | ||
| default: | ||
| return false; | ||
| } | ||
| } | ||
| md.inline.ruler.at("text", function (state, silent) { | ||
| var pos = state.pos; | ||
| while (pos < state.posMax && !isTerminatorCharExtended(state.src.charCodeAt(pos))) { | ||
| pos++; | ||
| } | ||
| if (pos === state.pos) return false; | ||
| if (!silent) state.pending += state.src.slice(state.pos, pos); | ||
| state.pos = pos; | ||
| return true; | ||
| }); | ||
|
|
||
| // ── Block: headings ────────────────────────────────────────────────────── | ||
| // = H1 = == H2 == === H3 === ==== H4 ==== ===== H5 ===== | ||
| // Rules: | ||
| // - The number of = signs must match on both sides (1–5) | ||
| // - At least one space between the = signs and the title text | ||
| // - Trailing spaces after the closing = signs are allowed | ||
| md.block.ruler.before( | ||
| "heading", | ||
| "txt2tags_heading", | ||
| function (state, startLine, endLine, silent) { | ||
| var pos = state.bMarks[startLine] + state.tShift[startLine]; | ||
| var max = state.eMarks[startLine]; | ||
|
|
||
| if (state.src.charCodeAt(pos) !== 0x3d /* = */) return false; | ||
|
|
||
| var line = state.src.slice(pos, max); | ||
| var match = /^(={1,5}) +(.+?) +\1\s*$/.exec(line); | ||
| if (!match) return false; | ||
|
|
||
| var level = match[1].length; | ||
| var title = match[2]; | ||
|
|
||
| if (silent) return true; | ||
|
|
||
| var token; | ||
| token = state.push("heading_open", "h" + level, 1); | ||
| token.markup = match[1]; | ||
| token.map = [startLine, startLine + 1]; | ||
|
|
||
| token = state.push("inline", "", 0); | ||
| token.content = title; | ||
| token.map = [startLine, startLine + 1]; | ||
| token.children = []; | ||
|
|
||
| token = state.push("heading_close", "h" + level, -1); | ||
| token.markup = match[1]; | ||
|
|
||
| state.line = startLine + 1; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Block: % comments ──────────────────────────────────────────────────── | ||
| // A line whose very first character is % is silently consumed. | ||
| md.block.ruler.before( | ||
| "paragraph", | ||
| "txt2tags_comment", | ||
| function (state, startLine, endLine, silent) { | ||
| // Use bMarks (not bMarks + tShift) to require % at column 0 | ||
| var pos = state.bMarks[startLine]; | ||
| if (state.src.charCodeAt(pos) !== 0x25 /* % */) return false; | ||
| if (silent) return true; | ||
| state.line = startLine + 1; | ||
| return true; | ||
| }, | ||
| { alt: ["paragraph"] } | ||
| ); | ||
|
|
||
| // ── Block: + numbered list ─────────────────────────────────────────────── | ||
| // Lines starting with '+ ' (plus space) form an ordered list. | ||
| // Registered BEFORE 'list' so markdown-it's own list rule doesn't consume +. | ||
| md.block.ruler.before( | ||
| "list", | ||
| "txt2tags_ordered_list", | ||
| function (state, startLine, endLine, silent) { | ||
| var pos = state.bMarks[startLine] + state.tShift[startLine]; | ||
| if (state.src.charCodeAt(pos) !== 0x2B /* + */ || | ||
| state.src.charCodeAt(pos + 1) !== 0x20 /* space */) return false; | ||
| if (silent) return true; | ||
|
|
||
| var items = []; | ||
| var line = startLine; | ||
| while (line < endLine) { | ||
| pos = state.bMarks[line] + state.tShift[line]; | ||
| if (state.src.charCodeAt(pos) !== 0x2B || | ||
| state.src.charCodeAt(pos + 1) !== 0x20) break; | ||
| items.push(state.src.slice(pos + 2, state.eMarks[line])); | ||
| line++; | ||
| } | ||
|
|
||
| var token = state.push("ordered_list_open", "ol", 1); | ||
| token.map = [startLine, line]; | ||
| token.markup = "+"; | ||
|
|
||
| for (var i = 0; i < items.length; i++) { | ||
| token = state.push("list_item_open", "li", 1); | ||
| token.map = [startLine + i, startLine + i + 1]; | ||
| token.markup = "+"; | ||
|
|
||
| token = state.push("inline", "", 0); | ||
| token.content = items[i]; | ||
| token.map = [startLine + i, startLine + i + 1]; | ||
| token.children = []; | ||
|
|
||
| state.push("list_item_close", "li", -1).markup = "+"; | ||
| } | ||
|
|
||
| state.push("ordered_list_close", "ol", -1).markup = "+"; | ||
| state.line = line; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Inline: //italic// ─────────────────────────────────────────────────── | ||
| // Avoid matching inside URLs (e.g. http://) | ||
| md.inline.ruler.push( | ||
| "txt2tags_italic", | ||
| function (state, silent) { | ||
| var pos = state.pos; | ||
| var src = state.src; | ||
| if (src.charCodeAt(pos) !== 0x2F || src.charCodeAt(pos + 1) !== 0x2F) return false; | ||
| if (pos > 0 && src.charCodeAt(pos - 1) === 0x3A /* : */) return false; | ||
| var start = pos + 2; | ||
| var end = src.indexOf("//", start); | ||
| if (end < 0 || end === start) return false; | ||
| if (!silent) { | ||
| state.push("em_open", "em", 1).markup = "//"; | ||
| state.push("text", "", 0).content = src.slice(start, end); | ||
| state.push("em_close", "em", -1).markup = "//"; | ||
| } | ||
| state.pos = end + 2; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Inline: __underline__ ──────────────────────────────────────────────── | ||
| // Registered BEFORE 'emphasis' so that __ is consumed here instead of | ||
| // being treated as markdown bold. | ||
| md.inline.ruler.before( | ||
| "emphasis", | ||
| "txt2tags_underline", | ||
| function (state, silent) { | ||
| var pos = state.pos; | ||
| var src = state.src; | ||
| if (src.charCodeAt(pos) !== 0x5F || src.charCodeAt(pos + 1) !== 0x5F) return false; | ||
| var start = pos + 2; | ||
| var end = src.indexOf("__", start); | ||
| if (end < 0 || end === start) return false; | ||
| if (!silent) { | ||
| state.push("txt2tags_u_open", "u", 1); | ||
| state.push("text", "", 0).content = src.slice(start, end); | ||
| state.push("txt2tags_u_close", "u", -1); | ||
| } | ||
| state.pos = end + 2; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Inline: [label url] links ──────────────────────────────────────────── | ||
| // Registered BEFORE 'link' so markdown-it's own link rule doesn't consume [. | ||
| // If the content matches [text](url) (standard markdown), we let the link | ||
| // rule handle it by returning false when a '(' immediately follows ']'. | ||
| md.inline.ruler.before( | ||
| "link", | ||
| "txt2tags_link", | ||
| function (state, silent) { | ||
| var pos = state.pos; | ||
| var src = state.src; | ||
| if (src.charCodeAt(pos) !== 0x5B /* [ */) return false; | ||
| var closePos = src.indexOf("]", pos + 1); | ||
| if (closePos < 0) return false; | ||
| // Let standard markdown [text](url) pass through | ||
| if (src.charCodeAt(closePos + 1) === 0x28 /* ( */) return false; | ||
| var content = src.slice(pos + 1, closePos); | ||
| // Last space separates label from URL | ||
| var lastSpace = content.lastIndexOf(" "); | ||
| if (lastSpace < 0) return false; | ||
| var label = content.slice(0, lastSpace); | ||
| var url = content.slice(lastSpace + 1); | ||
| if (!label || !url) return false; | ||
| // URL must start with a recognised scheme or / | ||
| if (!/^[a-zA-Z][\w+\-.]*:\/\/|^\//.test(url)) return false; | ||
| if (!md.validateLink(url)) return false; | ||
| var normalizedUrl = md.normalizeLink(url); | ||
| if (!silent) { | ||
| var token = state.push("link_open", "a", 1); | ||
| token.attrs = [["href", normalizedUrl]]; | ||
| token.markup = "txt2tags"; | ||
| state.push("text", "", 0).content = label; | ||
| state.push("link_close", "a", -1).markup = "txt2tags"; | ||
| } | ||
| state.pos = closePos + 1; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Inline: bare URLs ──────────────────────────────────────────────────── | ||
| // Matches scheme://... URLs that appear without brackets. | ||
| // Registered BEFORE the text rule so the text rule doesn't consume the | ||
| // leading letters (it stops at '/' but would first consume e.g. "http:"). | ||
| md.inline.ruler.before( | ||
| "text", | ||
| "txt2tags_autolink", | ||
| function (state, silent) { | ||
| var pos = state.pos; | ||
| var src = state.src; | ||
| // Must start with a URL scheme (letters then "://") | ||
| var match = /^[a-zA-Z][\w+\-.]*:\/\/[^\s\]]*/.exec(src.slice(pos)); | ||
| if (!match) return false; | ||
| var url = match[0]; | ||
| // Strip trailing punctuation that is unlikely to be part of the URL | ||
| url = url.replace(/[.,;:!?)]+$/, ""); | ||
| if (!url) return false; | ||
| if (!md.validateLink(url)) return false; | ||
| var normalizedUrl = md.normalizeLink(url); | ||
| if (!silent) { | ||
| var token = state.push("link_open", "a", 1); | ||
| token.attrs = [["href", normalizedUrl]]; | ||
| token.markup = "autolink"; | ||
| state.push("text", "", 0).content = url; | ||
| state.push("link_close", "a", -1).markup = "autolink"; | ||
| } | ||
| state.pos = pos + url.length; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Inline: [[wikilink]] and [[wikilink|description]] ──────────────────── | ||
| // Registered BEFORE 'txt2tags_link' (and therefore before 'link') so that | ||
| // the double-bracket syntax is consumed before any single-bracket rule. | ||
| md.inline.ruler.before( | ||
| "txt2tags_link", | ||
| "txt2tags_wikilink", | ||
| function (state, silent) { | ||
| var pos = state.pos; | ||
| var src = state.src; | ||
| // Must start with [[ | ||
| if (src.charCodeAt(pos) !== 0x5B || src.charCodeAt(pos + 1) !== 0x5B) return false; | ||
| var closePos = src.indexOf("]]", pos + 2); | ||
| if (closePos < 0) return false; | ||
| var content = src.slice(pos + 2, closePos); | ||
| if (!content) return false; | ||
| // Split on first '|' to get optional description | ||
| var pipePos = content.indexOf("|"); | ||
| var target, label; | ||
| if (pipePos >= 0) { | ||
| target = content.slice(0, pipePos); | ||
| label = content.slice(pipePos + 1); | ||
| } else { | ||
| target = content; | ||
| label = content; | ||
| } | ||
| if (!target) return false; | ||
| // Append .md so the QOwnNotes hook resolves to a note file path | ||
| var href = /\.md$/i.test(target) ? target : (target + ".md"); | ||
| if (!silent) { | ||
| var token = state.push("link_open", "a", 1); | ||
| token.attrs = [["href", href]]; | ||
| token.markup = "wikilink"; | ||
| state.push("text", "", 0).content = label; | ||
| state.push("link_close", "a", -1).markup = "wikilink"; | ||
| } | ||
| state.pos = closePos + 2; | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| // ── Inline: --strikethrough-- ───────────────────────────────────────────── | ||
| md.inline.ruler.push( | ||
| "txt2tags_strike", | ||
| function (state, silent) { | ||
| var pos = state.pos; | ||
| var src = state.src; | ||
| if (src.charCodeAt(pos) !== 0x2D || src.charCodeAt(pos + 1) !== 0x2D) return false; | ||
| var start = pos + 2; | ||
| var end = src.indexOf("--", start); | ||
| if (end < 0 || end === start) return false; | ||
| if (!silent) { | ||
| state.push("txt2tags_s_open", "del", 1); | ||
| state.push("text", "", 0).content = src.slice(start, end); | ||
| state.push("txt2tags_s_close", "del", -1); | ||
| } | ||
| state.pos = end + 2; | ||
| return true; | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| return txt2tagsPlugin; | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bare-URL autolink rule accepts any scheme and sets
hrefdirectly without callingmd.validateLink()/md.normalizeLink(). This can allow unsafe schemes (e.g.javascript://...) to be rendered as clickable links, bypassing markdown-it’s safety checks. Validate and normalize the URL before emittinglink_open, and reject untrusted protocols.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. The autolink rule now calls md.validateLink() (rejects javascript: and other unsafe schemes) and
md.normalizeLink() (percent-encodes the URL) before emitting the link_open token — consistent with the
txt2tags_link rule.