Compare commits

...

44 commits
1.0.0 ... main

Author SHA1 Message Date
Nathan
a5ad979881
Update LICENSE 2025-08-04 14:29:41 -05:00
Nathan Smith
4366eebfb6 🚀 release: Release v1.2.0 2025-03-08 18:25:57 -06:00
Nathan
0f809ae0a9
Merge pull request #170 from nathonius/feat/158/issue-number
 feat: #158 Always display issue number
2025-03-08 18:22:50 -06:00
Nathan Smith
2d9dba0d43 feat: #158 Always display issue number
Closes: #158
2025-03-08 15:49:05 -06:00
Nathan
568b4d4335
Merge pull request #168 from nathonius/release/1.1.0
🚀 release: Release v1.1.0
2025-02-26 23:38:44 -06:00
Nathan Smith
37ff39dbc5 🚀 release: Release v1.1.0 2025-02-26 23:36:51 -06:00
Nathan
ceab053dca
Merge pull request #167 from nathonius/feat/155/file-tag
 feat: #155 Show the full filename when linking directly to a file
2025-02-26 23:30:45 -06:00
Nathan Smith
dd3bfaa15f 🚨 test: #155 Add file sections to tests 2025-02-26 23:25:55 -06:00
Nathan Smith
1b4317c3b9 feat: #155 Show the full filename when linking directly to a file
Closes: #155
2025-02-26 23:23:21 -06:00
Nathan
3289da5a1d
Merge pull request #166 from nathonius/feat/154/sliders
 feat: #154 Add manual inputs for all slider settings
2025-02-26 22:50:08 -06:00
Nathan Smith
69f2d2a08e feat: #154 Add manual inputs for all slider settings
Closes: #154
2025-02-26 22:47:35 -06:00
Nathan Smith
a4d1cbc7bf 🚨 test: Add a test for a github.io page repo 2025-02-26 22:23:48 -06:00
Nathan
fef1eb84a0
Merge pull request #163 from nathonius/more-tests
More tests
2025-02-26 22:09:45 -06:00
Nathan Smith
6e143d00ce 🚨 test: Add more tests 2025-01-22 23:06:14 -05:00
Nathan Smith
ebb30a8940 🔨 refactor: Adjust test methodology 2025-01-22 00:17:37 -05:00
Nathan
020fcead39
Merge pull request #162 from nathonius/all-contributors/add-tmcw
docs: add tmcw as a contributor for code
2025-01-22 00:10:35 -05:00
allcontributors[bot]
088597bb8c
docs: update .all-contributorsrc 2025-01-22 05:08:25 +00:00
allcontributors[bot]
ed4bc6961f
docs: update README.md 2025-01-22 05:08:24 +00:00
Nathan
0878512754
Merge pull request #151 from tmcw/improve-inline-link-style
Improve inline link style
2025-01-22 00:07:32 -05:00
Nathan
82f5dadf83
Merge pull request #161 from nathonius/changelog-fix
🧹 chore: correct changelog link
2025-01-19 19:09:19 -05:00
Nathan Smith
353b95ec34 🧹 chore: correct changelog link 2025-01-19 19:07:43 -05:00
Nathan
cd7ebaf679
Merge pull request #160 from nathonius/all-contributors/add-joshleaves
docs: add joshleaves as a contributor for code, and test
2025-01-19 19:06:13 -05:00
allcontributors[bot]
84eeedb9ba
docs: update .all-contributorsrc 2025-01-20 00:04:25 +00:00
allcontributors[bot]
cae9718a6e
docs: update README.md 2025-01-20 00:04:24 +00:00
Nathan
640a042946
Merge pull request #159 from nathonius/release/1.0.3
🚀 release: Release v1.0.3
2025-01-19 19:01:11 -05:00
Nathan Smith
05cbb3dce2 🚀 release: Release v1.0.3 2025-01-19 18:59:34 -05:00
Nathan
9cdfba024d
Merge pull request #157 from joshleaves/fix/156-linking-to-file
Fix: Match links to a single file with their dot extensions
2025-01-19 18:53:27 -05:00
Nathan Smith
41f6c40bb3 remove unnecessary escapes 2025-01-19 18:50:40 -05:00
red
7679a149be Fix: Add more edge cases in tests and rework regexp 2025-01-18 02:22:51 +01:00
red
bf0572256a Fix: Don't capture first word after repo 2025-01-04 23:05:34 +01:00
red
0257f4909f Fix: Simplified Regexp 2024-12-30 21:01:12 +01:00
red
ea163a31e0 Fix: More URL subtleties, and some unit tests 2024-12-30 20:49:05 +01:00
red
42d35e4c70 Fix: Match links to a single file with their dot extensions 2024-12-20 09:48:00 +01:00
Tom MacWright
792ca07395
Improve inline link style 2024-09-09 17:32:46 -04:00
Nathan
10aabb2a34
Merge pull request #149 from nathonius/release/1.0.2
🚀 release: Release v1.0.2
2024-08-30 18:34:20 -04:00
Nathan Smith
b355284cf5 🚀 release: Release v1.0.2
Includes changes from 1.0.1 which was improperly released.
2024-08-30 18:33:05 -04:00
Nathan
be2aa15f32
Merge pull request #148 from nathonius/fix/142/oauth-404
🐛 fix: #142 Trim oauth client id of whitespace
2024-08-30 18:27:14 -04:00
Nathan Smith
0b38bd17f0 🐛 fix: #142 Trim oauth client id of whitespace
Was resulting in 404

Closes: #142
2024-08-30 17:42:49 -04:00
Nathan
7aa0c14e01
Merge pull request #147 from nathonius/fix/143/use-custom-oauth
🐛 fix: #143 Use custom oauth setting during token gen for new account
2024-08-30 17:35:11 -04:00
Nathan Smith
061b8bdab9 🐛 fix: #143 Use custom oauth setting during token gen for new account
Closes: #143
2024-08-30 17:33:38 -04:00
Nathan
4919fe23eb
Merge pull request #146 from nathonius/fix/144/tolerant-url
🐛 fix: #144 Tolerate badly formatted URLs
2024-08-30 17:25:28 -04:00
Nathan Smith
2d42cb3edf 🐛 fix: #144 Tolerate badly formatted URLs
URL constructor should not throw error anymore. Known special paths are ignored.

Closes: #144
2024-08-30 17:17:34 -04:00
Nathan
8d495d990d
Merge pull request #141 from nathonius/fix/139/display-text-links
🐛 fix: #139 Adjust url regex to exclude URLs with display text
2024-08-19 11:33:35 -04:00
Nathan Smith
0fb4e4dbd0 🐛 fix: #139 Adjust url regex to exclude URLs with display text
Closes: #139
2024-08-16 21:27:09 -04:00
24 changed files with 530 additions and 61 deletions

View file

@ -35,6 +35,25 @@
"contributions": [ "contributions": [
"bug" "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,6 +16,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- ## Unreleased --> <!-- ## 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 ## [1.0.0] - 2024-07-08
🔥 First major version update! 🔥 First major version update!
@ -127,6 +154,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Initial release. 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 [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.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 [0.7.1]: https://github.com/nathonius/obsidian-github-link/compare/0.7.0...0.7.1

42
LICENSE
View file

@ -1,24 +1,26 @@
This is free and unencumbered software released into the public domain. MIT NON-AI License
Anyone is free to copy, modify, publish, use, compile, sell, or Copyright (c) 2024, Nathan Smith
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors Permission is hereby granted, free of charge, to any person obtaining a copy of the software and associated documentation files (the "Software"),
of this software dedicate any and all copyright interest in the to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
software to the public domain. We make this dedication for the benefit 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.
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 SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
For more information, please refer to <https://unlicense.org> 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.

View file

@ -53,9 +53,11 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for info on contributing to the project.
<table> <table>
<tbody> <tbody>
<tr> <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="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="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="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://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="https://github.com/nathonius/obsidian-github-link/issues?q=author%3Anikclayton" title="Bug reports">🐛</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>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

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

View file

@ -0,0 +1,5 @@
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", "id": "github-link",
"name": "GitHub Link", "name": "GitHub Link",
"version": "1.0.0", "version": "1.2.0",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Enrich your notes with issue and pull request content from GitHub", "description": "Enrich your notes with issue and pull request content from GitHub",
"author": "Nathonius", "author": "Nathonius",

4
package-lock.json generated
View file

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

View file

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

View file

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

39
src/github/urls.ts Normal file
View file

@ -0,0 +1,39 @@
/**
* 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,6 +56,10 @@ export function setIssueIcon(icon: HTMLElement, status: IssueStatus): void {
icon.dataset.status = status; icon.dataset.status = status;
} }
export function setFileIcon(icon: HTMLElement): void {
setIcon(icon, "file");
}
export function setPRMergeableIcon(icon: HTMLElement, mergeable: boolean): void { export function setPRMergeableIcon(icon: HTMLElement, mergeable: boolean): void {
if (PluginSettings.tagTooltips) { if (PluginSettings.tagTooltips) {
icon.setAttribute("aria-label", PRMergeableText[`${mergeable}`]); icon.setAttribute("aria-label", PRMergeableText[`${mergeable}`]);

46
src/inline/inline.spec.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -83,13 +83,20 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
}); });
}) })
.addSlider((slider) => { .addSlider((slider) => {
const displayValue = createSpan({ text: PluginSettings.defaultPageSize.toString() }); const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
slider.sliderEl.parentElement?.prepend(displayValue); 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);
slider.setLimits(0, 30, 1); slider.setLimits(0, 30, 1);
slider.setDynamicTooltip(); slider.setDynamicTooltip();
slider.setValue(PluginSettings.defaultPageSize); slider.setValue(PluginSettings.defaultPageSize);
slider.onChange((value) => { slider.onChange((value) => {
displayValue.setText(value.toString()); manualInput.value = value.toString();
PluginSettings.defaultPageSize = value; PluginSettings.defaultPageSize = value;
void this.saveSettings(); void this.saveSettings();
}); });
@ -150,6 +157,28 @@ 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" }); containerEl.createEl("h3", { text: "Cache settings" });
new Setting(containerEl) new Setting(containerEl)
@ -169,14 +198,21 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
}); });
}) })
.addSlider((slider) => { .addSlider((slider) => {
const displayValue = createSpan({ text: PluginSettings.cacheIntervalSeconds.toString() }); const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
slider.sliderEl.parentElement?.prepend(displayValue); 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);
slider.setValue(PluginSettings.cacheIntervalSeconds); slider.setValue(PluginSettings.cacheIntervalSeconds);
slider.setLimits(10, 1200, 10); slider.setLimits(10, 1200, 10);
slider.setDynamicTooltip(); slider.setDynamicTooltip();
slider.onChange(async (value) => { slider.onChange(async (value) => {
PluginSettings.cacheIntervalSeconds = value; PluginSettings.cacheIntervalSeconds = value;
displayValue.setText(value.toString()); manualInput.value = value.toString();
await this.saveSettings(); await this.saveSettings();
this.plugin.setCacheInterval(); this.plugin.setCacheInterval();
}); });
@ -197,14 +233,21 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
}); });
}) })
.addSlider((slider) => { .addSlider((slider) => {
const displayValue = createSpan({ text: PluginSettings.maxCacheAgeHours.toString() }); const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
slider.sliderEl.parentElement?.prepend(displayValue); 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);
slider.setValue(PluginSettings.maxCacheAgeHours); slider.setValue(PluginSettings.maxCacheAgeHours);
slider.setLimits(0, 170, 10); slider.setLimits(0, 170, 10);
slider.setDynamicTooltip(); slider.setDynamicTooltip();
slider.onChange(async (value) => { slider.onChange(async (value) => {
PluginSettings.maxCacheAgeHours = value; PluginSettings.maxCacheAgeHours = value;
displayValue.setText(value.toString()); manualInput.value = value.toString();
await this.saveSettings(); await this.saveSettings();
}); });
}); });
@ -225,14 +268,21 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
}); });
}) })
.addSlider((slider) => { .addSlider((slider) => {
const displayValue = createSpan({ text: PluginSettings.minRequestSeconds.toString() }); const manualInput = createEl("input", { attr: { type: "number" }, cls: "github-link-slider-input" });
slider.sliderEl.parentElement?.prepend(displayValue); 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);
slider.setValue(PluginSettings.minRequestSeconds); slider.setValue(PluginSettings.minRequestSeconds);
slider.setLimits(10, 1200, 10); slider.setLimits(10, 1200, 10);
slider.setDynamicTooltip(); slider.setDynamicTooltip();
slider.onChange(async (value) => { slider.onChange(async (value) => {
PluginSettings.minRequestSeconds = value; PluginSettings.minRequestSeconds = value;
displayValue.setText(value.toString()); manualInput.value = value.toString();
await this.saveSettings(); await this.saveSettings();
}); });
}); });

View file

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

View file

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

66
test/element.ts Normal file
View file

@ -0,0 +1,66 @@
/**
* 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,4 +1,8 @@
{ {
"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", "1.0.0": "0.15.0",
"0.7.2": "0.15.0", "0.7.2": "0.15.0",
"0.7.1": "0.15.0", "0.7.1": "0.15.0",