KAN-73: document and validate MCP ticket description format
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"format": "Markdown stored verbatim and supported by the Tiptap WYSIWYG editor and ticket detail view.",
|
||||
"supported": "Supported: plain text, paragraphs, line breaks, headings, bold, italic, strikethrough, ordinary ordered/unordered lists (including nested lists), blockquotes, inline/fenced/indented code, horizontal rules, and safe links.",
|
||||
"unsupported": "Not visually supported: task checkboxes, tables, HTML, images, or unsafe links. Newly supplied incompatible descriptions reject the entire mutation with a description field error; existing descriptions are preserved when description is omitted.",
|
||||
"criteria": "Put acceptance criteria exclusively in the structured acceptance_criteria field. Do not repeat them in description as task checkboxes or a copied criteria list. No criteria are automatically extracted or moved.",
|
||||
"links": "Safe link destinations: http://, https://, mailto:, a root-relative path starting with a single /, or a #fragment. Protocol-relative URLs, other schemes, whitespace, control characters, and backslashes (including percent-encoded forms) are not allowed.",
|
||||
"example": "## Goal\n\nExplain the **context** and approach.\n\n- Implement the change\n- Verify [the guide](https://example.com/guide)\n\n> Keep the scope clear.\n\nUse `code` when needed.",
|
||||
"unsupported_tokens": {
|
||||
"html": "HTML",
|
||||
"table": "tables",
|
||||
"image": "images",
|
||||
"task": "task checkboxes",
|
||||
"unsafe_link": "unsafe links"
|
||||
},
|
||||
"link_prefix": "^(?:https?://|mailto:|/(?!/)|#)",
|
||||
"unsafe_link_characters": "[\\s\\\\\\x00-\\x1f\\x7f]"
|
||||
}
|
||||
@@ -2,30 +2,40 @@ import type { JSONContent } from '@tiptap/core';
|
||||
import { Markdown, MarkdownManager } from '@tiptap/markdown';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { h } from 'vue';
|
||||
import descriptionFormat from './ticket-description-format.json';
|
||||
import type { VNodeChild } from 'vue';
|
||||
|
||||
export function safeDescriptionLink(value: unknown): string | null {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
/[\s\\]/u.test(value) ||
|
||||
[...value].some(
|
||||
(character) =>
|
||||
character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
||||
)
|
||||
new RegExp(descriptionFormat.unsafe_link_characters, 'u').test(value)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (
|
||||
new RegExp(descriptionFormat.unsafe_link_characters, 'u').test(
|
||||
decodeURIComponent(value),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (/^(https?:\/\/|mailto:)/i.test(value)) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return ['http:', 'https:', 'mailto:'].includes(url.protocol)
|
||||
new URL(value);
|
||||
return new RegExp(descriptionFormat.link_prefix, 'i').test(value)
|
||||
? value
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return /^(\/(?!\/)|#)/.test(value) ? value : null;
|
||||
return new RegExp(descriptionFormat.link_prefix, 'i').test(value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
export function descriptionExtensions() {
|
||||
@@ -55,15 +65,21 @@ const markdown = new MarkdownManager({
|
||||
/** Unsupported Markdown stays editable verbatim instead of being silently discarded. */
|
||||
export function supportsVisualDescription(value: string): boolean {
|
||||
let supported = true;
|
||||
markdown.instance.walkTokens(markdown.instance.lexer(value), (token) => {
|
||||
if (
|
||||
['html', 'table', 'image'].includes(token.type) ||
|
||||
(token.type === 'list_item' && token.task) ||
|
||||
(token.type === 'link' && !safeDescriptionLink(token.href))
|
||||
) {
|
||||
supported = false;
|
||||
}
|
||||
});
|
||||
markdown.instance.walkTokens(
|
||||
markdown.instance.lexer(value, { gfm: true, breaks: true }),
|
||||
(token) => {
|
||||
if (
|
||||
Object.hasOwn(
|
||||
descriptionFormat.unsupported_tokens,
|
||||
token.type,
|
||||
) ||
|
||||
(token.type === 'list_item' && token.task) ||
|
||||
(token.type === 'link' && !safeDescriptionLink(token.href))
|
||||
) {
|
||||
supported = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
return supported;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user