Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

24 changed files with 61 additions and 530 deletions

View file

@ -35,25 +35,6 @@
"contributions": [
"bug"
]
},
{
"login": "joshleaves",
"name": "Arnaud 'red' Rouyer",
"avatar_url": "https://avatars.githubusercontent.com/u/1074760?v=4",
"profile": "https://tech.dreamleaves.org",
"contributions": [
"code",
"test"
]
},
{
"login": "tmcw",
"name": "Tom MacWright",
"avatar_url": "https://avatars.githubusercontent.com/u/32314?v=4",
"profile": "https://macwright.com/",
"contributions": [
"code"
]
}
]
}

View file

@ -16,33 +16,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- ## Unreleased -->
## [1.2.0] - 2025-03-08
### Changed
- Issue number is always shown on a tag for an issue
## [1.1.0] - 2025-02-26
### Changed
- Show rich metadata about filenames in links
- A manual input for all slider settings that allows settings outside of the normal bounds
## [1.0.3] - 2025-01-19
### Fixed
- Allow linking directly to single files, and generally improved reliability of regex. Thanks @joshleaves!
## [1.0.2] - 2024-08-30
### Fixed
- Better detection of urls that have display text and should not be rendered as tags
- More graceful handling of invalid URLs
- Don't attempt to render a tag for certain known special GitHub URLs
## [1.0.0] - 2024-07-08
🔥 First major version update!
@ -154,10 +127,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Initial release.
[1.2.0]: https://github.com/nathonius/obsidian-github-link/compare/1.1.0...1.2.0
[1.1.0]: https://github.com/nathonius/obsidian-github-link/compare/1.0.3...1.1.0
[1.0.3]: https://github.com/nathonius/obsidian-github-link/compare/1.0.2...1.0.3
[1.0.2]: https://github.com/nathonius/obsidian-github-link/compare/1.0.0...1.0.2
[1.0.0]: https://github.com/nathonius/obsidian-github-link/compare/0.7.2...1.0.0
[0.7.2]: https://github.com/nathonius/obsidian-github-link/compare/0.7.1...0.7.2
[0.7.1]: https://github.com/nathonius/obsidian-github-link/compare/0.7.0...0.7.1

42
LICENSE
View file

@ -1,26 +1,24 @@
MIT NON-AI License
This is free and unencumbered software released into the public domain.
Copyright (c) 2024, Nathan Smith
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
Permission is hereby granted, free of charge, to any person obtaining a copy of the software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
In addition, the following restrictions apply:
1. The Software and any modifications made to it may not be used for the purpose of training or improving machine learning algorithms,
including but not limited to artificial intelligence, natural language processing, or data mining. This condition applies to any derivatives,
modifications, or updates based on the Software code. Any usage of the Software in an AI-training dataset is considered a breach of this License.
2. The Software may not be included in any dataset used for training or improving machine learning algorithms,
including but not limited to artificial intelligence, natural language processing, or data mining.
3. Any person or organization found to be in violation of these restrictions will be subject to legal action and may be held liable
for any damages resulting from such use.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View file

@ -53,11 +53,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for info on contributing to the project.
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://nathan-smith.org"><img src="https://avatars.githubusercontent.com/u/4851889?v=4?s=100" width="100px;" alt="Nathan"/><br /><sub><b>Nathan</b></sub></a><br /><a href="#code-nathonius" title="Code">💻</a> <a href="#doc-nathonius" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://arongriffis.com"><img src="https://avatars.githubusercontent.com/u/50637?v=4?s=100" width="100px;" alt="Aron Griffis"/><br /><sub><b>Aron Griffis</b></sub></a><br /><a href="#code-agriffis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nikclayton"><img src="https://avatars.githubusercontent.com/u/773100?v=4?s=100" width="100px;" alt="Nik Clayton"/><br /><sub><b>Nik Clayton</b></sub></a><br /><a href="#bug-nikclayton" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tech.dreamleaves.org"><img src="https://avatars.githubusercontent.com/u/1074760?v=4?s=100" width="100px;" alt="Arnaud 'red' Rouyer"/><br /><sub><b>Arnaud 'red' Rouyer</b></sub></a><br /><a href="#code-joshleaves" title="Code">💻</a> <a href="#test-joshleaves" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://macwright.com/"><img src="https://avatars.githubusercontent.com/u/32314?v=4?s=100" width="100px;" alt="Tom MacWright"/><br /><sub><b>Tom MacWright</b></sub></a><br /><a href="#code-tmcw" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://nathan-smith.org"><img src="https://avatars.githubusercontent.com/u/4851889?v=4?s=100" width="100px;" alt="Nathan"/><br /><sub><b>Nathan</b></sub></a><br /><a href="https://github.com/nathonius/obsidian-github-link/commits?author=nathonius" title="Code">💻</a> <a href="https://github.com/nathonius/obsidian-github-link/commits?author=nathonius" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://arongriffis.com"><img src="https://avatars.githubusercontent.com/u/50637?v=4?s=100" width="100px;" alt="Aron Griffis"/><br /><sub><b>Aron Griffis</b></sub></a><br /><a href="https://github.com/nathonius/obsidian-github-link/commits?author=agriffis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nikclayton"><img src="https://avatars.githubusercontent.com/u/773100?v=4?s=100" width="100px;" alt="Nik Clayton"/><br /><sub><b>Nik Clayton</b></sub></a><br /><a href="https://github.com/nathonius/obsidian-github-link/issues?q=author%3Anikclayton" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View file

@ -3,7 +3,6 @@ import { PluginMock } from "./obsidian/Plugin";
import { ModalMock } from "./obsidian/Modal";
import { PluginSettingTabMock } from "./obsidian/PluginSettingTab";
import { NoticeMock } from "./obsidian/Notice";
import { setIconMock } from "./obsidian/setIcon";
module.exports = {
App: AppMock,
@ -11,5 +10,4 @@ module.exports = {
Modal: ModalMock,
PluginSettingTab: PluginSettingTabMock,
Notice: NoticeMock,
setIcon: setIconMock,
};

View file

@ -1,5 +0,0 @@
export function setIconMock(parent: HTMLElement, iconId: string): void {
const icon = document.createElement("span");
icon.innerText = iconId;
parent.appendChild(icon);
}

View file

@ -1,7 +1,7 @@
{
"id": "github-link",
"name": "GitHub Link",
"version": "1.2.0",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Enrich your notes with issue and pull request content from GitHub",
"author": "Nathonius",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-github-link",
"version": "1.2.0",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-github-link",
"version": "1.2.0",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@octokit/auth-oauth-device": "^6.0.1",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-github-link",
"version": "1.2.0",
"version": "1.0.0",
"description": "An Obsidian plugin enriching notes with issue and pull request content from GitHub",
"main": "main.js",
"scripts": {

View file

@ -1,5 +1,3 @@
import { logger } from "../plugin";
export interface ParsedUrl {
url: string;
host: string;
@ -10,8 +8,6 @@ export interface ParsedUrl {
code?: {
branch?: string;
path?: string;
filename?: string;
line?: string;
};
commit?: string;
}
@ -22,7 +18,7 @@ export function repoAPIToBrowserUrl(urlString: string): string {
return urlString.replace(apiRegex, "https://github.com/");
}
export function parseUrl(urlString: string): ParsedUrl | null {
export function parseUrl(urlString: string): ParsedUrl {
// Some potential URLs:
// https://github.com/nathonius/alloy-theme
// https://github.com/nathonius/obsidian-trello/issues/45
@ -32,14 +28,7 @@ export function parseUrl(urlString: string): ParsedUrl | null {
// https://github.com/nathonius/obsidian-trello/blob/markdown/src/constants.ts#L44-L58
// https://github.com/nathonius/obsidian-trello/commit/7ea069dd0641441ec20fb194f50e746a21abbaf1
// https://github.com/nathonius/obsidian-trello/commit/7ea069dd0641441ec20fb194f50e746a21abbaf1#diff-8fa4b52909f895e8cda060d2035234e0a42ca2c7d3f8f8de1b35a056537bf199R35
let url: URL;
try {
url = new URL(urlString);
} catch (err) {
logger.error(`Attempted and failed to parse invalid URL: ${urlString}`);
return null;
}
const url = new URL(urlString);
const parsedUrl: ParsedUrl = { url: urlString, host: url.hostname };
const urlParts = url.pathname.split("/");
@ -62,8 +51,6 @@ export function parseUrl(urlString: string): ParsedUrl | null {
if (urlParts[5]) {
const pathParts = urlParts.slice(5);
parsedUrl.code.path = pathParts.join("/");
parsedUrl.code.filename = pathParts.last();
parsedUrl.code.line = url.hash.slice(1);
}
break;
case "commit":

View file

@ -1,39 +0,0 @@
/**
* These are paths that begin with `https://github.com` but should not
* be processed by the plugin.
*/
const knownPaths = [
"about",
"account",
"codespaces",
"dashboard",
"discussions",
"explore",
"gist",
"github-copilot",
"issues",
"logout",
"marketplace",
"mine",
"new",
"organizations",
"projects",
"pulls",
"search",
"security",
"settings",
"sponsors",
];
export function isAllowedPath(link: string): boolean {
try {
const url = new URL(link);
const basePath = url.pathname.split("/")[1];
if (knownPaths.includes(basePath)) {
return false;
}
} catch (err) {
return false;
}
return true;
}

View file

@ -56,10 +56,6 @@ export function setIssueIcon(icon: HTMLElement, status: IssueStatus): void {
icon.dataset.status = status;
}
export function setFileIcon(icon: HTMLElement): void {
setIcon(icon, "file");
}
export function setPRMergeableIcon(icon: HTMLElement, mergeable: boolean): void {
if (PluginSettings.tagTooltips) {
icon.setAttribute("aria-label", PRMergeableText[`${mergeable}`]);

View file

@ -1,46 +0,0 @@
import { test, describe, expect, beforeAll } from "@jest/globals";
import { elementTestSetup } from "../../test/element";
import { createTag } from "./inline";
const selectors = {
org: ".github-link-inline-org",
repo: ".github-link-inline-repo",
issueTitle: ".github-link-inline-issue-title",
prTitle: ".github-link-inline-pr-title",
file: ".github-link-inline-file",
};
describe("createTag", () => {
beforeAll(() => {
elementTestSetup();
});
test.each([
{ url: "https://github.com/nathonius", user: "nathonius" },
{ url: "https://github.com/nathonius/", user: "nathonius" },
{ url: "https://github.com/sam.lake", user: "sam.lake" },
{ url: "https://github.com/abraham_lincoln", user: "abraham_lincoln" },
])("user only $user", ({ url, user }) => {
const tag = createTag(url);
expect(tag).toBeTruthy();
const org = tag?.querySelector(selectors.org) as HTMLSpanElement;
expect(org.innerText).toEqual(user);
[selectors.repo, selectors.issueTitle, selectors.prTitle, selectors.file].forEach((s) => {
expect(tag?.querySelector(s)).toBeFalsy();
});
});
test.each([
{ url: "https://github.com/nathonius/obsidian-github-link", user: "nathonius", repo: "obsidian-github-link" },
{ url: "https://github.com/nathonius/obsidian.github.link", user: "nathonius", repo: "obsidian.github.link" },
])("repo $repo", ({ url, user, repo }) => {
const tag = createTag(url);
expect(tag).toBeTruthy();
const org = tag?.querySelector(selectors.org) as HTMLSpanElement;
const repoEl = tag?.querySelector(selectors.repo) as HTMLSpanElement;
expect(org.innerText).toEqual(user);
expect(repoEl.innerText).toEqual(repo);
[selectors.issueTitle, selectors.prTitle, selectors.file].forEach((s) => {
expect(tag?.querySelector(s)).toBeFalsy();
});
});
});

View file

@ -1,6 +1,6 @@
import { setIcon } from "obsidian";
import { IssueStatus, getIssueStatus, getPRStatus } from "../github/response";
import { setFileIcon, setIssueIcon, setPRIcon, setPRMergeableIcon } from "../icon";
import { setIssueIcon, setPRIcon, setPRMergeableIcon } from "../icon";
import { PluginSettings } from "../plugin";
import type { PullResponse } from "../github/response";
@ -8,19 +8,14 @@ import { RequestError } from "../util";
import { parseUrl } from "../github/url-parse";
import type { ParsedUrl } from "../github/url-parse";
import { getIssue, getPullRequest } from "../github/github";
import { isAllowedPath } from "../github/urls";
interface TagConfig {
icon: HTMLSpanElement;
sections: HTMLElement[];
}
export function createTag(href: string): HTMLAnchorElement | null {
export function createTag(href: string): HTMLAnchorElement {
const parsedUrl = parseUrl(href);
if (!parsedUrl) {
return null;
}
const container = createEl("a", { cls: "github-link-inline", href, attr: { target: "_blank" } });
const config: TagConfig = {
icon: createSpan({ cls: ["github-link-status-icon", "github-link-inline-icon"] }),
@ -31,8 +26,8 @@ export function createTag(href: string): HTMLAnchorElement | null {
createOrgSection(config, parsedUrl);
createRepoSection(config, parsedUrl);
// Add issue OR pr OR file
if (parsedUrl.issue !== undefined || parsedUrl.pr !== undefined || parsedUrl.code !== undefined) {
// Add issue OR pr
if (parsedUrl.issue !== undefined || parsedUrl.pr !== undefined) {
// Remove org
const orgIndex = config.sections.findIndex((section) => section.classList.contains("github-link-inline-org"));
if (orgIndex !== -1) {
@ -43,8 +38,6 @@ export function createTag(href: string): HTMLAnchorElement | null {
createIssueSection(config, parsedUrl, container);
} else if (parsedUrl.pr !== undefined) {
createPullRequestSection(config, parsedUrl, container);
} else if (parsedUrl.code !== undefined) {
createFileSection(config, parsedUrl, container);
}
}
@ -98,18 +91,17 @@ function createIssueSection(config: TagConfig, parsedUrl: ParsedUrl, container:
}
setIssueIcon(config.icon, IssueStatus.Open);
const issueContainer = createSpan({
cls: "github-link-inline-issue-title-section",
cls: "github-link-inline-issue-title",
text: `${parsedUrl.issue}`,
});
issueContainer.createSpan({ text: `#${parsedUrl.issue}`, cls: "github-link-inline-issue-number" });
config.sections.push(issueContainer);
if (parsedUrl.org && parsedUrl.repo) {
getIssue(parsedUrl.org, parsedUrl.repo, parsedUrl.issue)
.then((issue) => {
if (issue.title) {
const title = createSpan({ text: issue.title, cls: "github-link-inline-issue-title" });
issueContainer.prepend(title);
const status = getIssueStatus(issue);
setIssueIcon(config.icon, status);
issueContainer.setText(issue.title);
}
})
.catch((err) => {
@ -144,28 +136,6 @@ function createPullRequestSection(config: TagConfig, parsedUrl: ParsedUrl, conta
}
}
function createFileSection(config: TagConfig, parsedUrl: ParsedUrl, _container: HTMLAnchorElement) {
if (parsedUrl.code === undefined) {
return;
}
const fileContainer = createSpan({ cls: "github-link-inline-file" });
setFileIcon(config.icon);
config.sections.push(fileContainer);
if (parsedUrl.code.filename) {
fileContainer.setText(parsedUrl.code.filename);
} else if (parsedUrl.code.path) {
fileContainer.setText(parsedUrl.code.path);
}
if (PluginSettings.tagShowFileLineNumber && parsedUrl.code.line) {
fileContainer.appendChild(createSpan({ cls: "github-link-inline-file-line-number", text: parsedUrl.code.line }));
}
if (PluginSettings.tagShowFileBranchName && parsedUrl.code.branch) {
fileContainer.appendChild(
createSpan({ cls: "github-link-inline-file-branch", text: `(${parsedUrl.code.branch})` }),
);
}
}
/**
* Note that this function is called AFTER the tag has been built, so it adds itself to the dom.
*/
@ -192,17 +162,11 @@ function createErrorSection(config: TagConfig, container: HTMLAnchorElement, err
}
export function InlineRenderer(el: HTMLElement) {
let githubLinks = Array.from(el.querySelectorAll<HTMLAnchorElement>(`a.external-link[href^="https://github.com"]`));
// Filter out some special URLs from github
githubLinks = githubLinks.filter((l) => isAllowedPath(l.href));
const githubLinks = el.querySelectorAll<HTMLAnchorElement>(`a.external-link[href^="https://github.com"]`);
for (const anchor of Array.from(githubLinks)) {
if (anchor.href === anchor.innerText) {
const container = createTag(anchor.href);
if (container) {
anchor.replaceWith(container);
}
anchor.replaceWith(container);
}
}
}

View file

@ -1,103 +0,0 @@
import { expect, test, describe } from "@jest/globals";
import { matchRegexp } from "./view-plugin";
// Some potential URLs:
const GITHUB_URLS = [
// USER/ORG
"https://github.com/nathonius",
// REPO
"https://github.com/nathonius/obsidian-github-link",
"https://github.com/nathonius/obsidian-github-link/tree/main",
"https://github.com/joshleaves/obsidian-github-link/tree/fix/156-linking-to-file",
"https://github.com/nathonius/obsidian-github-link/blob/main/src/github/url-parse.ts",
"https://github.com/nathonius/obsidian-github-link/blob/main/src/inline/view-plugin.ts#L21",
"https://github.com/nathonius/obsidian-github-link/blob/main/src/inline/view-plugin.ts#L13-32",
"https://github.com/academicpages/academicpages.github.io",
// ISSUES
"https://github.com/nathonius/obsidian-github-link/issues",
"https://github.com/nathonius/obsidian-github-link/issues/156",
// PULLS
"https://github.com/nathonius/obsidian-github-link/pulls",
"https://github.com/nathonius/obsidian-github-link/pull/157",
"https://github.com/nathonius/obsidian-github-link/pull/157/commits",
"https://github.com/nathonius/obsidian-github-link/pull/157/commits/42d35e4c7070d2ec9f3bacf7f4a0561d9d7346bb",
"https://github.com/nathonius/obsidian-github-link/pull/157/files",
// MORE EDGE CASES
"https://github.com/kwsch/pk3DS/blob/e40d3ce5548d75821f31785dc88cd465610530a6/pk3DS.Core/CTR/Images/BCLIM.cs",
"https://github.com/kwsch/png2bclim/blob/master/png2bclim/BCLIM.cs",
"https://github.com/kwsch/png2bclim",
"https://github.com/ihaveamac/3DS-rom-tools/wiki/Extract-a-game-or-application-in-.3ds-or-.cci-format",
];
describe("url regex match", () => {
test.each(GITHUB_URLS)("matches GitHub URLs on their own", (url) => {
const text = `${url}`; // (...)
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("does not match URLs inside markdown links", (url) => {
const text = `[A link to GitHub](${url})`;
const match = text.match(matchRegexp);
expect(match).toBeNull();
});
test.each(GITHUB_URLS)("matches URLs ending a sentence", (url) => {
const text = `That's the end of the line for ${url}.`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs followed by punctuation", (url) => {
const text = `First ${url}, then some text.`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs ending a sentence with an interrogative", (url) => {
const text = `You heard about ${url}?`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs followed by multiple punctuation", (url) => {
const text = `A test for ${url}...`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs ending a sentence that segues to another", (url) => {
const text = `That's the end of the line for ${url}. But not for this variable!`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs in text with multiple lines", (url) => {
const text = `Some text
${url}
more text.`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs in text followed by another text", (url) => {
const text = `${url} is a cool plugin`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
test.each(GITHUB_URLS)("matches URLs quoted", (url) => {
const text = `Obama chuckled: you mean "${url}" ?`;
const match = text.match(matchRegexp);
expect(match).not.toBeNull();
expect(match![0]).toBe(url);
});
});

View file

@ -3,7 +3,6 @@ import type { DecorationSet, PluginValue, EditorView, ViewUpdate } from "@codemi
import { Decoration, MatchDecorator, ViewPlugin, WidgetType } from "@codemirror/view";
import type { GithubLinkPlugin } from "../plugin";
import { isAllowedPath } from "../github/urls";
import { createTag } from "./inline";
interface DecoSpec {
@ -29,15 +28,11 @@ class InlineTagWidget extends WidgetType {
toDOM(): HTMLElement {
const container = createSpan();
const tag = createTag(this.href);
if (tag) {
container.appendChild(tag);
}
container.appendChild(tag);
return container;
}
}
export const matchRegexp = /(?<!]\()https:\/\/github\.com\/[^\s]+(?<!['".?,:;]+)/g;
export function createInlineViewPlugin(_plugin: GithubLinkPlugin) {
class InlineViewPluginValue implements PluginValue {
/**
@ -49,7 +44,7 @@ export function createInlineViewPlugin(_plugin: GithubLinkPlugin) {
public inlineTags: DecorationSet = Decoration.none;
private readonly matcher = new MatchDecorator({
regexp: matchRegexp,
regexp: /(https:\/\/)?github\.com\S+/g,
decorate: (add, from, to, match, _view) => {
add(
from,
@ -88,11 +83,6 @@ export function createInlineViewPlugin(_plugin: GithubLinkPlugin) {
return false;
}
// Check if it's not an allowed URL
if (!isAllowedPath(match[0])) {
return false;
}
// Check if we're in a codeblock
// Note, codeblock check is more expensive than some others,
// But we do it first to ensure this.inCodeblock remains accurate
@ -126,7 +116,7 @@ export function createInlineViewPlugin(_plugin: GithubLinkPlugin) {
}
// Ignore matches inside a markdown link
// TODO: This can probably be removed now that the regex is updated
// TODO: This could probably be a regex.
const input = match.input ?? "";
const index = match.index ?? 0;
const matchValue = match[0];

View file

@ -57,7 +57,7 @@ export const CommonIssuePRColumns: ColumnsMap = {
el.classList.add("github-link-table-repo");
const url = repoAPIToBrowserUrl((row as IssueListResponse[number]).repository_url);
const parsed = parseUrl(url);
el.createEl("a", { text: parsed?.repo ?? "Repo", href: url, attr: { target: "_blank" } });
el.createEl("a", { text: parsed.repo, href: url, attr: { target: "_blank" } });
},
},
author: {

View file

@ -36,9 +36,7 @@ export const IssueColumns: ColumnsMap = {
return;
}
const tag = createTag(pullRequestUrl);
if (tag) {
el.appendChild(tag);
}
el.appendChild(tag);
},
},
};

View file

@ -90,7 +90,7 @@ export class AccountSettings {
text.setValue(this.newAccount!.clientId ?? "");
text.setPlaceholder("Client ID");
text.onChange((value) => {
this.newAccount!.clientId = value.trim();
this.newAccount!.clientId = value;
});
});
}
@ -103,11 +103,7 @@ export class AccountSettings {
.addButton((button) => {
button.setButtonText("Generate Token");
button.onClick(async () => {
const clientId = this.newAccount?.customOAuth ? this.newAccount.clientId : undefined;
const authResult = await auth(
this.tokenVerification.bind(this),
clientId,
)({
const authResult = await auth(this.tokenVerification.bind(this))({
type: "oauth",
});
this.authModal?.close();
@ -120,7 +116,7 @@ export class AccountSettings {
text.setPlaceholder("Personal Access Token / OAuth Token");
text.setValue(this.newAccount!.token);
text.onChange((value) => {
this.newAccount!.token = value.trim();
this.newAccount!.token = value;
});
});
@ -195,7 +191,7 @@ export class AccountSettings {
text.setValue(account.clientId ?? "");
text.setPlaceholder("Client ID");
text.onChange((value) => {
account.clientId = value.trim();
account.clientId = value;
void this.saveCallback();
});
});
@ -227,7 +223,7 @@ export class AccountSettings {
text.setPlaceholder("Personal Access Token / OAuth Token");
text.setValue(account.token);
text.onChange((value) => {
account.token = value.trim();
account.token = value;
void this.saveCallback();
});
});

View file

@ -83,20 +83,13 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
});
})
.addSlider((slider) => {
const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
manualInput.value = PluginSettings.defaultPageSize.toString();
manualInput.addEventListener("change", (e) => {
const value = parseInt((e.target as HTMLInputElement).value, 10);
PluginSettings.defaultPageSize = value;
slider.setValue(value);
void this.saveSettings();
});
slider.sliderEl.parentElement?.prepend(manualInput);
const displayValue = createSpan({ text: PluginSettings.defaultPageSize.toString() });
slider.sliderEl.parentElement?.prepend(displayValue);
slider.setLimits(0, 30, 1);
slider.setDynamicTooltip();
slider.setValue(PluginSettings.defaultPageSize);
slider.onChange((value) => {
manualInput.value = value.toString();
displayValue.setText(value.toString());
PluginSettings.defaultPageSize = value;
void this.saveSettings();
});
@ -157,28 +150,6 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
});
});
new Setting(containerEl)
.setName("File branch name")
.setDesc("Append the branch name to links to an individual file inside a repo")
.addToggle((toggle) => {
toggle.setValue(PluginSettings.tagShowFileBranchName);
toggle.onChange((value) => {
PluginSettings.tagShowFileBranchName = value;
void this.saveSettings();
});
});
new Setting(containerEl)
.setName("File line number")
.setDesc("Append the line number (if provided) to links to an individual file inside a repo")
.addToggle((toggle) => {
toggle.setValue(PluginSettings.tagShowFileLineNumber);
toggle.onChange((value) => {
PluginSettings.tagShowFileLineNumber = value;
void this.saveSettings();
});
});
containerEl.createEl("h3", { text: "Cache settings" });
new Setting(containerEl)
@ -198,21 +169,14 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
});
})
.addSlider((slider) => {
const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
manualInput.value = PluginSettings.cacheIntervalSeconds.toString();
manualInput.addEventListener("change", (e) => {
const value = parseInt((e.target as HTMLInputElement).value, 10);
PluginSettings.cacheIntervalSeconds = value;
slider.setValue(value);
void this.saveSettings();
});
slider.sliderEl.parentElement?.prepend(manualInput);
const displayValue = createSpan({ text: PluginSettings.cacheIntervalSeconds.toString() });
slider.sliderEl.parentElement?.prepend(displayValue);
slider.setValue(PluginSettings.cacheIntervalSeconds);
slider.setLimits(10, 1200, 10);
slider.setDynamicTooltip();
slider.onChange(async (value) => {
PluginSettings.cacheIntervalSeconds = value;
manualInput.value = value.toString();
displayValue.setText(value.toString());
await this.saveSettings();
this.plugin.setCacheInterval();
});
@ -233,21 +197,14 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
});
})
.addSlider((slider) => {
const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
manualInput.value = PluginSettings.maxCacheAgeHours.toString();
manualInput.addEventListener("change", (e) => {
const value = parseInt((e.target as HTMLInputElement).value, 10);
PluginSettings.maxCacheAgeHours = value;
slider.setValue(value);
void this.saveSettings();
});
slider.sliderEl.parentElement?.prepend(manualInput);
const displayValue = createSpan({ text: PluginSettings.maxCacheAgeHours.toString() });
slider.sliderEl.parentElement?.prepend(displayValue);
slider.setValue(PluginSettings.maxCacheAgeHours);
slider.setLimits(0, 170, 10);
slider.setDynamicTooltip();
slider.onChange(async (value) => {
PluginSettings.maxCacheAgeHours = value;
manualInput.value = value.toString();
displayValue.setText(value.toString());
await this.saveSettings();
});
});
@ -268,21 +225,14 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
});
})
.addSlider((slider) => {
const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
manualInput.value = PluginSettings.minRequestSeconds.toString();
manualInput.addEventListener("change", (e) => {
const value = parseInt((e.target as HTMLInputElement).value, 10);
PluginSettings.minRequestSeconds = value;
slider.setValue(value);
void this.saveSettings();
});
slider.sliderEl.parentElement?.prepend(manualInput);
const displayValue = createSpan({ text: PluginSettings.minRequestSeconds.toString() });
slider.sliderEl.parentElement?.prepend(displayValue);
slider.setValue(PluginSettings.minRequestSeconds);
slider.setLimits(10, 1200, 10);
slider.setDynamicTooltip();
slider.onChange(async (value) => {
PluginSettings.minRequestSeconds = value;
manualInput.value = value.toString();
displayValue.setText(value.toString());
await this.saveSettings();
});
});

View file

@ -27,8 +27,6 @@ export interface GithubLinkPluginSettings {
logLevel: LogLevel;
tagTooltips: boolean;
tagShowPRMergeable: boolean;
tagShowFileBranchName: boolean;
tagShowFileLineNumber: boolean;
cacheIntervalSeconds: number;
maxCacheAgeHours: number;
minRequestSeconds: number;
@ -43,8 +41,6 @@ export const DEFAULT_SETTINGS: GithubLinkPluginSettings = {
logLevel: LogLevel.Error,
tagTooltips: false,
tagShowPRMergeable: false,
tagShowFileBranchName: true,
tagShowFileLineNumber: true,
cacheIntervalSeconds: 60,
maxCacheAgeHours: 120,
minRequestSeconds: 60,

View file

@ -151,10 +151,6 @@ body.theme-dark {
padding-left: 2rem;
}
.github-link-slider-input {
width: 3rem;
}
.github-link-inline {
display: inline-flex;
background-color: var(--gh-color-canvas-default);
@ -180,14 +176,6 @@ body.theme-dark {
overflow: hidden;
}
.github-link-inline-section:first-child {
border-radius: 3.5px 0 0 3.5px;
}
.github-link-inline-section:last-child {
border-radius: 0 3.5px 3.5px 0;
}
.github-link-inline-icon {
display: inline-flex;
padding: 2px 0;
@ -277,27 +265,12 @@ body.theme-dark {
}
.github-link-inline-pr-title,
.github-link-inline-issue-title,
.github-link-inline-issue-title-section,
.github-link-inline-file {
.github-link-inline-issue-title {
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
line-height: var(--line-height-normal);
}
.github-link-inline-issue-number {
color: var(--gh-color-fg-muted);
}
.github-link-inline-file,
.github-link-inline-issue-title-section {
display: flex;
gap: 2px;
}
.github-link-inline-file > .github-link-inline-file-line-number,
.github-link-inline-file > .github-link-inline-file-branch {
color: var(--gh-color-fg-muted);
}
.github-link-table-wrapper {
margin-block-end: var(--p-spacing);

View file

@ -1,66 +0,0 @@
/**
* Local version of obsidian's 'createEl' function, copied from
* https://ryotaushio.github.io/the-hobbyist-dev/obsidian/api-&-internals/createel.html
*/
function createEl<K extends keyof HTMLElementTagNameMap>(
tag: K,
options?: string | DomElementInfo,
callback?: (el: HTMLElementTagNameMap[K]) => void,
): HTMLElementTagNameMap[K] {
const el = document.createElement(tag);
if (typeof options === "string") options = { cls: options };
options = options || {};
if (options.cls) Array.isArray(options.cls) ? (el.className = options.cls.join(" ")) : (el.className = options.cls);
if (options.text) el.setText(options.text);
if (options.attr) {
for (const [k, v] of Object.entries(options.attr)) {
el.setAttribute(k, String(v));
}
}
if (options.title !== undefined) el.title = options.title;
if (
options.value !== undefined &&
(el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLOptionElement)
) {
el.value = options.value;
}
if (options.type && el instanceof HTMLInputElement) el.type = options.type;
if (options.type && el instanceof HTMLStyleElement) el.setAttribute("type", options.type);
if (options.placeholder && el instanceof HTMLInputElement) el.placeholder = options.placeholder;
if (
options.href &&
(el instanceof HTMLAnchorElement || el instanceof HTMLLinkElement || el instanceof HTMLBaseElement)
)
el.href = options.href;
callback?.(el);
if (options.parent) {
if (options.prepend) options.parent.insertBefore(el, options.parent.firstChild);
else options.parent.appendChild(el);
}
// add event listeners (e.g. { onclick: () => {} })
for (const key in options) {
if (Object.prototype.hasOwnProperty.call(options, key) && key.startsWith("on")) {
const value = options[key as keyof DomElementInfo];
if (typeof value === "function") el.addEventListener(key.substring(2), value);
}
}
return el;
}
function createSpan(options?: string | DomElementInfo, callback?: (el: HTMLSpanElement) => void) {
return createEl("span", options, callback);
}
function createDiv(options?: string | DomElementInfo, callback?: (el: HTMLDivElement) => void) {
return createEl("div", options, callback);
}
export function elementTestSetup() {
HTMLElement.prototype.setText = function (text: string) {
this.innerText = text;
};
window.createEl = createEl;
window.createSpan = createSpan;
window.createDiv = createDiv;
}

View file

@ -1,8 +1,4 @@
{
"1.2.0": "0.15.0",
"1.1.0": "0.15.0",
"1.0.3": "0.15.0",
"1.0.2": "0.15.0",
"1.0.0": "0.15.0",
"0.7.2": "0.15.0",
"0.7.1": "0.15.0",