Created
October 28, 2025 09:23
-
-
Save suin/ead99a9e25e9d1bd43e420c62595bf5b to your computer and use it in GitHub Desktop.
LinearのURLをきれいにするやつ
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
| import {fromMarkdown} from 'mdast-util-from-markdown'; | |
| import {toMarkdown} from 'mdast-util-to-markdown'; | |
| import {visitParents} from 'unist-util-visit-parents'; | |
| import type {Link} from 'mdast'; | |
| import {gfmFromMarkdown, gfmToMarkdown} from 'mdast-util-gfm'; | |
| import {gfm as micromarkGfm} from 'micromark-extension-gfm'; | |
| import {frontmatterFromMarkdown, frontmatterToMarkdown} from 'mdast-util-frontmatter'; | |
| import {frontmatter as micromarkFrontmatter} from 'micromark-extension-frontmatter'; | |
| import {directiveFromMarkdown, directiveToMarkdown} from 'mdast-util-directive'; | |
| import {directive as micromarkDirective} from 'micromark-extension-directive'; | |
| export function linearCleanLinks(src: string): string { | |
| const tree = fromMarkdown(src, { | |
| extensions: [ | |
| micromarkGfm(), | |
| micromarkFrontmatter(['yaml', 'toml']), | |
| micromarkDirective() | |
| ], | |
| mdastExtensions: [ | |
| gfmFromMarkdown(), | |
| frontmatterFromMarkdown(['yaml', 'toml']), | |
| directiveFromMarkdown() | |
| ] | |
| }); | |
| function cleanLinearUrl(url: string): string { | |
| // Only handle absolute https? urls to linear.app | |
| let u: URL | undefined; | |
| try { | |
| u = new URL(url); | |
| } catch { | |
| return url; | |
| } | |
| if (u.hostname !== 'linear.app' && u.hostname !== 'www.linear.app') return url; | |
| const path = u.pathname; // e.g. /org/issue/ABC-123/some-title | |
| const m = path.match(/^\/(?:([^\/]+))\/(?:issue|issues)\/([A-Z]+-\d+)(?:\/.*)?$/); | |
| if (!m) return url; | |
| const org = m[1]; | |
| const key = m[2]; | |
| const cleaned = `https://linear.app/${org}/issue/${key}`; | |
| return cleaned; | |
| } | |
| visitParents(tree as any, 'link', (node) => { | |
| const link = node as Link; | |
| if (typeof link.url !== 'string') return; | |
| const cleaned = cleanLinearUrl(link.url); | |
| if (cleaned !== link.url) { | |
| link.url = cleaned; | |
| } | |
| }); | |
| const markdown = toMarkdown(tree, { | |
| bullet: '-', | |
| fences: true, | |
| emphasis: '_', | |
| rule: '-', | |
| unsafe: [], | |
| extensions: [ | |
| gfmToMarkdown(), | |
| frontmatterToMarkdown(['yaml', 'toml']), | |
| directiveToMarkdown() | |
| ] | |
| }); | |
| return markdown.trimEnd(); | |
| } | |
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
| import { describe, expect, it } from "bun:test"; | |
| import { linearCleanLinks } from "."; | |
| describe("linearCleanLinks", () => { | |
| it("removes issue title from linear link (issue singular)", () => { | |
| const input = "[ABC-123: some title](https://linear.app/appthrust/issue/ABC-123/some-title)"; | |
| const output = linearCleanLinks(input); | |
| expect(output).toBe("[ABC-123: some title](https://linear.app/appthrust/issue/ABC-123)"); | |
| }); | |
| it("removes issue title from linear link (issues plural)", () => { | |
| const input = "[ABC-123: some title](https://linear.app/appthrust/issues/ABC-123/some-title)"; | |
| const output = linearCleanLinks(input); | |
| expect(output).toBe("[ABC-123: some title](https://linear.app/appthrust/issue/ABC-123)"); | |
| }); | |
| it("keeps non-linear links unchanged", () => { | |
| const input = "[example](https://example.com/path)"; | |
| const output = linearCleanLinks(input); | |
| expect(output).toBe(input.trimEnd()); | |
| }); | |
| it("keeps linear links that already have no title unchanged", () => { | |
| const input = "[ABC-123: title](https://linear.app/appthrust/issue/ABC-123)"; | |
| const output = linearCleanLinks(input); | |
| expect(output).toBe(input.trimEnd()); | |
| }); | |
| it("ignores invalid urls or relative links", () => { | |
| const input = "[x](not-a-url) [y](/relative)"; | |
| const output = linearCleanLinks(input); | |
| expect(output).toBe(input.trimEnd()); | |
| }); | |
| it("works inside paragraphs and lists", () => { | |
| const input = "- item [ABC-1](https://linear.app/org/issues/ABC-1/some)\n\ntext [ABC-2](https://linear.app/org/issue/ABC-2/another)"; | |
| const output = linearCleanLinks(input); | |
| expect(output).toBe("- item [ABC-1](https://linear.app/org/issue/ABC-1)\n\ntext [ABC-2](https://linear.app/org/issue/ABC-2)"); | |
| }); | |
| }); | |
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
| { | |
| "name": "my-app", | |
| "version": "0.1.0", | |
| "private": true, | |
| "scripts": { | |
| "dev": "next dev", | |
| "build": "next build", | |
| "start": "next start", | |
| "lint": "eslint", | |
| "test": "bun test" | |
| }, | |
| "dependencies": { | |
| "@radix-ui/react-slot": "^1.2.3", | |
| "class-variance-authority": "^0.7.1", | |
| "clsx": "^2.1.1", | |
| "lucide-react": "^0.548.0", | |
| "mdast-util-directive": "^3.1.0", | |
| "mdast-util-from-markdown": "^2.0.2", | |
| "mdast-util-frontmatter": "^2.0.1", | |
| "mdast-util-gfm": "^3.1.0", | |
| "mdast-util-to-markdown": "^2.1.2", | |
| "micromark-extension-directive": "^4.0.0", | |
| "micromark-extension-frontmatter": "^2.0.0", | |
| "micromark-extension-gfm": "^3.0.0", | |
| "next": "16.0.0", | |
| "react": "19.2.0", | |
| "react-dom": "19.2.0", | |
| "remark-parse": "^11.0.0", | |
| "remark-stringify": "^11.0.0", | |
| "tailwind-merge": "^3.3.1", | |
| "unified": "^11.0.5", | |
| "unist-util-visit": "^5.0.0", | |
| "unist-util-visit-parents": "^6.0.2" | |
| }, | |
| "devDependencies": { | |
| "@tailwindcss/postcss": "^4", | |
| "@types/node": "^20", | |
| "@types/react": "^19", | |
| "@types/react-dom": "^19", | |
| "eslint": "^9", | |
| "eslint-config-next": "16.0.0", | |
| "tailwindcss": "^4", | |
| "tw-animate-css": "^1.4.0", | |
| "typescript": "^5" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment