mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
Compare commits
44 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5ad979881 | ||
|
|
4366eebfb6 | ||
|
|
0f809ae0a9 | ||
|
|
2d9dba0d43 | ||
|
|
568b4d4335 | ||
|
|
37ff39dbc5 | ||
|
|
ceab053dca | ||
|
|
dd3bfaa15f | ||
|
|
1b4317c3b9 | ||
|
|
3289da5a1d | ||
|
|
69f2d2a08e | ||
|
|
a4d1cbc7bf | ||
|
|
fef1eb84a0 | ||
|
|
6e143d00ce | ||
|
|
ebb30a8940 | ||
|
|
020fcead39 | ||
|
|
088597bb8c | ||
|
|
ed4bc6961f | ||
|
|
0878512754 | ||
|
|
82f5dadf83 | ||
|
|
353b95ec34 | ||
|
|
cd7ebaf679 | ||
|
|
84eeedb9ba | ||
|
|
cae9718a6e | ||
|
|
640a042946 | ||
|
|
05cbb3dce2 | ||
|
|
9cdfba024d | ||
|
|
41f6c40bb3 | ||
|
|
7679a149be | ||
|
|
bf0572256a | ||
|
|
0257f4909f | ||
|
|
ea163a31e0 | ||
|
|
42d35e4c70 | ||
|
|
792ca07395 | ||
|
|
10aabb2a34 | ||
|
|
b355284cf5 | ||
|
|
be2aa15f32 | ||
|
|
0b38bd17f0 | ||
|
|
7aa0c14e01 | ||
|
|
061b8bdab9 | ||
|
|
4919fe23eb | ||
|
|
2d42cb3edf | ||
|
|
8d495d990d | ||
|
|
0fb4e4dbd0 |
24 changed files with 530 additions and 61 deletions
|
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -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
42
LICENSE
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
5
__mocks__/obsidian/setIcon.ts
Normal file
5
__mocks__/obsidian/setIcon.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function setIconMock(parent: HTMLElement, iconId: string): void {
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.innerText = iconId;
|
||||||
|
parent.appendChild(icon);
|
||||||
|
}
|
||||||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
39
src/github/urls.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
46
src/inline/inline.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
src/inline/view-plugin.spec.ts
Normal file
103
src/inline/view-plugin.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
29
styles.css
29
styles.css
|
|
@ -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
66
test/element.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue