mirror of
https://github.com/kdnk/obsidian-automatic-linker.git
synced 2026-07-22 05:37:46 +00:00
Merge pull request #18 from kdnk/handle-linear-url
feat: handle Linear URLs and format them in Obsidian link style
This commit is contained in:
commit
3272b980de
6 changed files with 268 additions and 0 deletions
19
src/main.ts
19
src/main.ts
|
|
@ -15,6 +15,7 @@ import { getTitleFromHtml } from "./replace-url-with-title/utils/get-title-from-
|
|||
import { listupAllUrls } from "./replace-url-with-title/utils/list-up-all-urls";
|
||||
import { formatGitHubURL } from "./replace-urls/github";
|
||||
import { formatJiraURL } from "./replace-urls/jira";
|
||||
import { formatLinearURL } from "./replace-urls/linear";
|
||||
import { replaceURLs } from "./replace-urls/replace-urls";
|
||||
import { AutomaticLinkerPluginSettingsTab } from "./settings/settings";
|
||||
import {
|
||||
|
|
@ -70,6 +71,16 @@ export default class AutomaticLinkerPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.settings.formatLinearURLs) {
|
||||
await this.app.vault.process(activeFile, (fileContent) => {
|
||||
return replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatLinearURL,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.settings.replaceUrlWithTitle) {
|
||||
const fileContent = await this.app.vault.read(activeFile);
|
||||
const { contentStart } = getFrontMatterInfo(fileContent);
|
||||
|
|
@ -198,6 +209,14 @@ export default class AutomaticLinkerPlugin extends Plugin {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.settings.formatLinearURLs) {
|
||||
lineText = replaceURLs(
|
||||
cm.getLine(line),
|
||||
this.settings,
|
||||
formatLinearURL,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.settings.replaceUrlWithTitle) {
|
||||
const fileContent = await this.app.vault.read(activeFile);
|
||||
const { contentStart } = getFrontMatterInfo(fileContent);
|
||||
|
|
|
|||
116
src/replace-urls/__tests__/replace-urls.linear.test.ts
Normal file
116
src/replace-urls/__tests__/replace-urls.linear.test.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
AutomaticLinkerSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "../../settings/settings-info";
|
||||
import { formatLinearURL } from "../linear";
|
||||
|
||||
describe("formatLinearURL", () => {
|
||||
const baseSettings: AutomaticLinkerSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
formatLinearURLs: true,
|
||||
};
|
||||
|
||||
describe("Basic Linear URL formatting", () => {
|
||||
it("should format Linear issue URL with title", () => {
|
||||
const input =
|
||||
"https://linear.app/andrewmcodes/issue/ACME-123/title-of-issue";
|
||||
const expected =
|
||||
"[[linear/andrewmcodes/ACME-123]] [🔗](https://linear.app/andrewmcodes/issue/ACME-123)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should format Linear issue URL without title", () => {
|
||||
const input = "https://linear.app/workspace/issue/PROJ-456";
|
||||
const expected =
|
||||
"[[linear/workspace/PROJ-456]] [🔗](https://linear.app/workspace/issue/PROJ-456)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should format linear:// protocol URL", () => {
|
||||
const input = "linear://andrewmcodes/issue/ACME-123";
|
||||
const expected =
|
||||
"[[linear/andrewmcodes/ACME-123]] [🔗](https://linear.app/andrewmcodes/issue/ACME-123)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should not modify non-Linear URLs", () => {
|
||||
const input = "https://example.com/issue/ACME-123";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(input);
|
||||
});
|
||||
|
||||
it("should handle invalid URLs", () => {
|
||||
const input = "not-a-url";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("URL pattern validation", () => {
|
||||
it("should not format URLs without issue path", () => {
|
||||
const input = "https://linear.app/workspace";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(input);
|
||||
});
|
||||
|
||||
it("should not format URLs with invalid issue format", () => {
|
||||
const input = "https://linear.app/workspace/issue/INVALID";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(input);
|
||||
});
|
||||
|
||||
it("should format URLs with different workspace names", () => {
|
||||
const input = "https://linear.app/my-company/issue/BUG-789";
|
||||
const expected =
|
||||
"[[linear/my-company/BUG-789]] [🔗](https://linear.app/my-company/issue/BUG-789)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Issue ID format validation", () => {
|
||||
it("should handle issue IDs with numbers", () => {
|
||||
const input = "https://linear.app/workspace/issue/ABC-123";
|
||||
const expected =
|
||||
"[[linear/workspace/ABC-123]] [🔗](https://linear.app/workspace/issue/ABC-123)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle issue IDs with multiple digits", () => {
|
||||
const input = "https://linear.app/workspace/issue/PROJ-99999";
|
||||
const expected =
|
||||
"[[linear/workspace/PROJ-99999]] [🔗](https://linear.app/workspace/issue/PROJ-99999)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should not format issue IDs without hyphen", () => {
|
||||
const input = "https://linear.app/workspace/issue/ABC123";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("URL with query parameters", () => {
|
||||
it("should format URLs with query parameters", () => {
|
||||
const input =
|
||||
"https://linear.app/workspace/issue/ACME-123?something=value";
|
||||
const expected =
|
||||
"[[linear/workspace/ACME-123]] [🔗](https://linear.app/workspace/issue/ACME-123)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should format URLs with hash fragments", () => {
|
||||
const input =
|
||||
"https://linear.app/workspace/issue/ACME-123#comment-123";
|
||||
const expected =
|
||||
"[[linear/workspace/ACME-123]] [🔗](https://linear.app/workspace/issue/ACME-123)";
|
||||
expect(formatLinearURL(input, baseSettings)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("When formatLinearURLs is disabled", () => {
|
||||
it("should not format Linear URLs when setting is false", () => {
|
||||
const settingsDisabled: AutomaticLinkerSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
formatLinearURLs: false,
|
||||
};
|
||||
const input = "https://linear.app/workspace/issue/ACME-123";
|
||||
expect(formatLinearURL(input, settingsDisabled)).toBe(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { AutomaticLinkerSettings } from "../settings/settings-info";
|
||||
import { formatJiraURL } from "./jira";
|
||||
import { formatLinearURL } from "./linear";
|
||||
|
||||
type GitHubURLInfo = {
|
||||
owner: string;
|
||||
|
|
@ -141,5 +142,12 @@ export function formatURL(
|
|||
}
|
||||
}
|
||||
|
||||
if (settings.formatLinearURLs) {
|
||||
const formattedLinearURL = formatLinearURL(url, settings);
|
||||
if (formattedLinearURL !== url) {
|
||||
return formattedLinearURL;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
|||
104
src/replace-urls/linear.ts
Normal file
104
src/replace-urls/linear.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { AutomaticLinkerSettings } from "../settings/settings-info";
|
||||
|
||||
type LinearURLInfo = {
|
||||
workspace: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a Linear URL by converting to Obsidian link format:
|
||||
* [[linear/workspace/ISSUE-123]] [🔗](url)
|
||||
* @param url The URL to format
|
||||
* @param settings Plugin settings
|
||||
* @returns The formatted URL in Obsidian link format
|
||||
*/
|
||||
export function formatLinearURL(
|
||||
url: string,
|
||||
settings: AutomaticLinkerSettings,
|
||||
): string {
|
||||
// Check if Linear URL formatting is enabled
|
||||
if (!settings.formatLinearURLs) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle linear:// protocol by converting to https://
|
||||
let processedURL = url;
|
||||
if (url.startsWith("linear://")) {
|
||||
processedURL = url.replace("linear://", "https://linear.app/");
|
||||
}
|
||||
|
||||
const linearURL = new URL(processedURL);
|
||||
|
||||
// Check if it's a Linear URL
|
||||
if (!isLinearURL(linearURL)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const urlInfo = parseLinearURL(linearURL);
|
||||
if (!urlInfo) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const cleanURL = getCleanURL(urlInfo);
|
||||
return formatToObsidianLink(urlInfo, cleanURL);
|
||||
} catch (e) {
|
||||
// If URL is invalid, return original string
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Linear URL into its components
|
||||
*/
|
||||
function parseLinearURL(url: URL): LinearURLInfo | null {
|
||||
const parts = url.pathname.split("/").filter(Boolean);
|
||||
|
||||
// Expected pattern: /workspace/issue/ISSUE-123 or /workspace/issue/ISSUE-123/title
|
||||
if (parts.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [workspace, type, issueId] = parts;
|
||||
|
||||
// Check if the URL matches the expected pattern
|
||||
if (type !== "issue") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate issue ID format (should be like PROJ-123)
|
||||
const issueIdPattern = /^[A-Z]+-\d+$/;
|
||||
if (!issueIdPattern.test(issueId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
workspace,
|
||||
issueId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clean URL without query parameters, hash fragments, and title
|
||||
*/
|
||||
function getCleanURL(urlInfo: LinearURLInfo): string {
|
||||
return `https://linear.app/${urlInfo.workspace}/issue/${urlInfo.issueId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format URL info into Obsidian link format
|
||||
*/
|
||||
function formatToObsidianLink(
|
||||
urlInfo: LinearURLInfo,
|
||||
cleanURL: string,
|
||||
): string {
|
||||
const wikiLink = `[[linear/${urlInfo.workspace}/${urlInfo.issueId}]] [🔗](${cleanURL})`;
|
||||
return wikiLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the URL is a Linear URL
|
||||
*/
|
||||
function isLinearURL(url: URL): boolean {
|
||||
return url.hostname === "linear.app";
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ export type AutomaticLinkerSettings = {
|
|||
githubEnterpriseURLs: string[]; // List of GitHub Enterprise URLs
|
||||
formatJiraURLs: boolean; // Format Jira URLs on save
|
||||
jiraURLs: string[]; // List of Jira URLs (domains)
|
||||
formatLinearURLs: boolean; // Format Linear URLs on save
|
||||
debug: boolean; // Enable debug logging
|
||||
ignoreCase: boolean; // Ignore case when matching links
|
||||
replaceUrlWithTitle: boolean; // Replace raw URLs with [Title](URL)
|
||||
|
|
@ -32,6 +33,7 @@ export const DEFAULT_SETTINGS: AutomaticLinkerSettings = {
|
|||
githubEnterpriseURLs: [], // Default: empty list
|
||||
formatJiraURLs: true, // Default: format Jira URLs
|
||||
jiraURLs: [], // Default: empty list
|
||||
formatLinearURLs: true, // Default: format Linear URLs
|
||||
debug: false, // Default: disable debug logging
|
||||
ignoreCase: false, // Default: case-sensitive matching
|
||||
replaceUrlWithTitle: false, // Default: disable replacing URLs with titles
|
||||
|
|
|
|||
|
|
@ -263,6 +263,25 @@ export class AutomaticLinkerPluginSettingsTab extends PluginSettingTab {
|
|||
text.inputEl.cols = 50;
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL Formatting for Linear")
|
||||
.setHeading();
|
||||
|
||||
// Toggle for formatting Linear URLs on save
|
||||
new Setting(containerEl)
|
||||
.setName("Format Linear URLs on save")
|
||||
.setDesc(
|
||||
"When enabled, Linear URLs will be formatted when saving the file.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.formatLinearURLs)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.formatLinearURLs = value;
|
||||
await this.plugin.saveData(this.plugin.settings);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL Replacement with Title")
|
||||
.setHeading();
|
||||
|
|
|
|||
Loading…
Reference in a new issue