diff --git a/apps/extension/FIREFOX_STORE_LISTING.md b/apps/extension/FIREFOX_STORE_LISTING.md index 2caf968d..7825970b 100644 --- a/apps/extension/FIREFOX_STORE_LISTING.md +++ b/apps/extension/FIREFOX_STORE_LISTING.md @@ -3,7 +3,7 @@ ## Listing - Name: EdgeEver Web Clipper -- Summary: Save a webpage, selected text, an image, or an X post to your EdgeEver instance. +- Summary: Save a webpage, selected text, an image, an X post, or a GitHub repository to your EdgeEver instance. - Category: Bookmarks - Homepage: https://edgeever.org - Support: https://github.com/tianma-if/edgeever/issues @@ -11,18 +11,19 @@ ## Description -EdgeEver Web Clipper saves the current webpage, selected text, a right-clicked image, or an X post directly to the self-hosted EdgeEver instance you configure. +EdgeEver Web Clipper saves the current webpage, selected text, a right-clicked image, an X post, or a GitHub repository's address and description directly to the self-hosted EdgeEver instance you configure. - Extract readable article content, or right-click selected text and save that passage only. - Right-click an image and save that image file as a new note. From Google Search, the note records Google Search and the keyword. - On X, right-click the post text and save the full post, including text behind Show more, plus photos already shown. +- On a GitHub repository page or code tree, right-click the page background, the description, or the README text and save the repository address and description. - These commands stay on the top-level right-click menu. - Convert captured HTML to Markdown locally. - Choose a default EdgeEver notebook. - Send content directly to your instance without an EdgeEver-operated relay. - No advertising, analytics, tracking, or telemetry. -Before using the extension, enter your EdgeEver instance URL and API token in the extension settings. The extension reads and sends page content only after you click **Clip current page**, **Save selection to EdgeEver**, **Save image to EdgeEver**, or **Save this post to EdgeEver**. +Before using the extension, enter your EdgeEver instance URL and API token in the extension settings. The extension reads and sends page content only after you click **Clip current page**, **Save selection to EdgeEver**, **Save image to EdgeEver**, **Save this post to EdgeEver**, or **Save this repository to EdgeEver**. ## Data collection and transmission @@ -42,7 +43,8 @@ The project maintainers do not receive or retain this data. Instance settings ar 6. On a normal webpage, select a passage, right-click it, choose **Save selection to EdgeEver**, and verify the note contains that passage and the page link. 7. On a normal webpage, right-click an image, choose **Save image to EdgeEver**, and verify the new note contains the image. 8. Open one X post page, right-click the post text (not a photo), choose **Save this post to EdgeEver**, and verify the note contains the post text and the post link. -9. Verify the created notes in the review EdgeEver instance. +9. Open one GitHub repository page, right-click the description or README text, choose **Save this repository to EdgeEver**, and verify the note contains the repository address and description. +10. Verify the created notes in the review EdgeEver instance. Restricted browser pages, extension stores, built-in PDF viewers, and other privileged pages cannot be captured. diff --git a/apps/extension/README.md b/apps/extension/README.md index 7d9e7cf1..2ec483bc 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -11,6 +11,7 @@ Chrome, Edge, and Firefox Manifest V3 extension for saving the current webpage, - Right-click an image and choose **Save image to EdgeEver**. The image file is uploaded into a new note in the default notebook, with the source page linked underneath. An image saved from Google Search records “Google Search” and the search keyword instead of the results URL. - Select text on a page, right-click, and choose **Save selection to EdgeEver**. That passage is saved as a new note in the default notebook, with the source page linked underneath. The rest of the page is not included. - On X, right-click the post text and choose **Save this post to EdgeEver**. The post text, author, time, and photos already shown in that post are saved into one note. Text folded behind Show more is expanded first, so a long post is saved in full. Right-clicking a photo saves that image instead, so the command stays on the top-level menu. A single-post page can be saved immediately. On a timeline, the extension asks once for access to X so it can remember the post under the pointer. +- On a GitHub repository page or code tree, right-click the page background, the description, or the README text and choose **Save this repository to EdgeEver**. The note keeps the repository address, the About text, and the homepage, language, license, and topics when the page shows them. An empty About falls back to the README's first paragraph. Issue, discussion, and single-file pages are left unsaved. The toolbar action uses the same card on those repository pages. Right-clicking a link or an image keeps the existing selection and image commands, so this one stays on the top-level menu. - When the page cannot hand over the image bytes, the extension asks once for access to that image's site. After that, later images from the same site save directly. - Create a searchable EdgeEver memo with the source URL and a `web-clip` tag. diff --git a/apps/extension/STORE_LISTING.md b/apps/extension/STORE_LISTING.md index d46cb34c..92bd8051 100644 --- a/apps/extension/STORE_LISTING.md +++ b/apps/extension/STORE_LISTING.md @@ -27,11 +27,11 @@ Select the matching language in the Chrome Web Store developer dashboard and ent #### Summary -将当前网页、选中文字、右键图片或 X 推文保存到你自托管的 EdgeEver 实例。 +将当前网页、选中文字、右键图片、X 推文或 GitHub 仓库保存到你自托管的 EdgeEver 实例。 #### Detailed description -EdgeEver Web Clipper 可以把当前网页、选中的文字、右键选中的图片,或 X 上的一条推文保存到自托管的 EdgeEver 实例。 +EdgeEver Web Clipper 可以把当前网页、选中的文字、右键选中的图片、X 上的一条推文,或 GitHub 仓库的地址和简介保存到自托管的 EdgeEver 实例。 主要功能: @@ -39,12 +39,13 @@ EdgeEver Web Clipper 可以把当前网页、选中的文字、右键选中的 - 选中一段文字后右键,选择“保存选中文字到 EdgeEver”,只保存这段文字。 - 在图片上右键,选择“保存图片到 EdgeEver”,把图片文件存成一条新笔记。 - 在 X 上右键推文正文,选择“保存这条推文到 EdgeEver”。长文会先展开“显示更多”,再保存全文和已经显示的图片。 +- 在 GitHub 仓库主页或代码目录上,右键页面空白、简介或 README 正文,选择“保存这个仓库到 EdgeEver”。笔记保留仓库地址和简介;页面上有主页、语言、许可证和话题时一并写入。 - 这几项命令直接出现在右键菜单的第一级。 - 在笔记中保留来源和剪藏时间。从 Google 搜索保存的图片记录“Google 搜索”和关键词。 - 可选择默认笔记本,并自动添加 `web-clip` 标签。 - 网页内容直接发送到你配置的 EdgeEver 实例,不经过开发者的中转服务器。 -使用前,请在插件设置中填写 EdgeEver 实例地址和 API Token。插件只会在你点击“剪藏当前网页”,或选择“保存选中文字到 EdgeEver”“保存图片到 EdgeEver”“保存这条推文到 EdgeEver”后读取当前标签页。图片若无法由页面直接交出,才会再向你请求该图片所在网站的访问权限。 +使用前,请在插件设置中填写 EdgeEver 实例地址和 API Token。插件只会在你点击“剪藏当前网页”,或选择“保存选中文字到 EdgeEver”“保存图片到 EdgeEver”“保存这条推文到 EdgeEver”“保存这个仓库到 EdgeEver”后读取当前标签页。图片若无法由页面直接交出,才会再向你请求该图片所在网站的访问权限。 EdgeEver 是开源、自托管的现代笔记工作区。项目主页与源代码:https://github.com/tianma-if/edgeever @@ -52,11 +53,11 @@ EdgeEver 是开源、自托管的现代笔记工作区。项目主页与源代 #### Summary -Save a webpage, selected text, a right-clicked image, or an X post to your self-hosted EdgeEver. +Save a webpage, selected text, a right-clicked image, an X post, or a GitHub repository to your self-hosted EdgeEver. #### Detailed description -EdgeEver Web Clipper saves the current webpage, selected text, a right-clicked image, or an X post to your self-hosted EdgeEver instance. +EdgeEver Web Clipper saves the current webpage, selected text, a right-clicked image, an X post, or a GitHub repository's address and description to your self-hosted EdgeEver instance. Key features: @@ -64,12 +65,13 @@ Key features: - Select text, right-click, and choose “Save selection to EdgeEver” to store that passage only. - Right-click an image and choose “Save image to EdgeEver” to store the image file as a new note. - On X, right-click the post text and choose “Save this post to EdgeEver”. Long posts are expanded before saving, so the full text and already shown photos go into one note. +- On a GitHub repository page or code tree, right-click the page background, the description, or the README text and choose “Save this repository to EdgeEver”. The note keeps the address and description, plus the homepage, language, license, and topics when the page shows them. - These commands stay on the top-level right-click menu. - Preserve the source and clipping time in the note. An image saved from Google Search records “Google Search” and the keyword. - Select a default notebook and add the `web-clip` tag automatically. - Send webpage content directly to your configured EdgeEver instance without a developer-operated relay server. -Before using the extension, enter your EdgeEver instance URL and API token in the extension settings. The extension reads the current tab only after you click “Clip current page” or choose “Save selection to EdgeEver”, “Save image to EdgeEver”, or “Save this post to EdgeEver”. It asks for access to an image's site only when that page cannot provide the image file. +Before using the extension, enter your EdgeEver instance URL and API token in the extension settings. The extension reads the current tab only after you click “Clip current page” or choose “Save selection to EdgeEver”, “Save image to EdgeEver”, “Save this post to EdgeEver”, or “Save this repository to EdgeEver”. It asks for access to an image's site only when that page cannot provide the image file. EdgeEver is an open-source, self-hosted modern notes workspace. Project homepage and source code: https://github.com/tianma-if/edgeever @@ -77,12 +79,12 @@ EdgeEver is an open-source, self-hosted modern notes workspace. Project homepage ### Single purpose -Save the current webpage, user-selected text, a user-chosen image, or one X post to the self-hosted EdgeEver instance explicitly configured by the user. +Save the current webpage, user-selected text, a user-chosen image, one X post, or one GitHub repository card to the self-hosted EdgeEver instance explicitly configured by the user. ### Permission justifications -- `activeTab`: Read the active page only after the user clicks the extension's save action or chooses Save selection to EdgeEver, Save image to EdgeEver, or Save this post to EdgeEver. -- `contextMenus`: Add one top-level item for the thing the user right-clicked: selected text, an image, or an X post. It runs only after the user selects that item. +- `activeTab`: Read the active page only after the user clicks the extension's save action or chooses Save selection to EdgeEver, Save image to EdgeEver, Save this post to EdgeEver, or Save this repository to EdgeEver. +- `contextMenus`: Add one top-level item for the thing the user right-clicked: selected text, an image, an X post, or a GitHub repository page. It runs only after the user selects that item. - `scripting`: Inject the packaged capture script into the active page after the user initiates a capture. - `storage`: Store the user's EdgeEver instance URL, API token, and default notebook ID locally. - Optional host permissions: Send API requests only to the EdgeEver instance origin the user approves. If a page cannot provide an image file, the extension can also ask for access to that image's site, or to all sites when the user explicitly chooses that option, and uses it only to download the image the user chose to save. Saving a post from an X timeline asks for access to X so the extension can remember which post was under the pointer. The script on X only records that target and runs after the user allows it. diff --git a/apps/extension/github-repo.test.mjs b/apps/extension/github-repo.test.mjs new file mode 100644 index 00000000..a27beda4 --- /dev/null +++ b/apps/extension/github-repo.test.mjs @@ -0,0 +1,162 @@ +import { describe, expect, test } from "bun:test"; +import { + cleanRepoDescription, + githubRepoFactsFromSource, + githubRepoNoteMarkdown, + githubRepoNoteTitle, + githubRepoTarget, + readmeLead, +} from "./src/github-repo.ts"; + +const labels = { + capturedAt: "2026-10-01T00:00:00.000Z", + sourceLabel: "来源", + capturedAtLabel: "抓取时间", + homepageLabel: "主页", + languageLabel: "语言", + licenseLabel: "许可证", + topicsLabel: "话题", +}; + +const source = (overrides = {}) => ({ + pageUrl: "https://github.com/tianma-if/edgeever", + openGraphDescription: "", + embeddedJsonChunks: [], + aboutText: "", + homepage: "", + topics: [], + license: "", + language: "", + readmeParagraphs: [], + ...overrides, +}); + +const sidebarPayload = (overrides = {}) => JSON.stringify({ + payload: { + sidebarAbout: { + description: "Open-source, AI-native knowledge base.", + website: "https://edgeever.org", + topics: [{ name: "notes" }, { name: "mcp" }, { name: "notes" }], + ownerLogin: "tianma-if", + repoName: "edgeever", + stargazerCount: 1915, + forksCount: 995, + repo: { license: { spdxId: "AGPL-3.0", name: "GNU Affero General Public License v3.0" } }, + ...overrides, + }, + }, +}); + +describe("github repository addresses", () => { + test("accepts a repository home and a code tree, and ignores other GitHub pages", () => { + expect(githubRepoTarget("https://github.com/tianma-if/edgeever")).toEqual({ + owner: "tianma-if", + name: "edgeever", + canonicalUrl: "https://github.com/tianma-if/edgeever", + }); + expect(githubRepoTarget("https://www.github.com/tianma-if/edgeever/?tab=readme-ov-file#readme")?.canonicalUrl) + .toBe("https://github.com/tianma-if/edgeever"); + expect(githubRepoTarget("https://github.com/tianma-if/edgeever/tree/main")?.canonicalUrl) + .toBe("https://github.com/tianma-if/edgeever"); + expect(githubRepoTarget("https://github.com/tianma-if/edgeever/tree/release/1.0/apps")?.canonicalUrl) + .toBe("https://github.com/tianma-if/edgeever"); + expect(githubRepoTarget("https://github.com/tianma-if/edgeever/issues/12")).toBeNull(); + expect(githubRepoTarget("https://github.com/tianma-if/edgeever/pull/4")).toBeNull(); + expect(githubRepoTarget("https://github.com/tianma-if/edgeever/blob/main/README.md")).toBeNull(); + expect(githubRepoTarget("https://github.com/tianma-if/edgeever/tree")).toBeNull(); + expect(githubRepoTarget("https://github.com/tianma-if")).toBeNull(); + expect(githubRepoTarget("https://gist.github.com/tianma-if/edgeever")).toBeNull(); + expect(githubRepoTarget("https://example.com/tianma-if/edgeever")).toBeNull(); + }); +}); + +describe("github repository card", () => { + test("drops GitHub's generated description and the repository suffix", () => { + expect(cleanRepoDescription( + "Open-source notes. - tianma-if/edgeever", + "tianma-if", + "edgeever", + )).toBe("Open-source notes."); + expect(cleanRepoDescription( + "Open-source notes & sync. - edgeever/apps at main · tianma-if/edgeever", + "tianma-if", + "edgeever", + )).toBe("Open-source notes & sync."); + expect(cleanRepoDescription( + "Contribute to tianma-if/edgeever development by creating an account on GitHub.", + "tianma-if", + "edgeever", + )).toBe(""); + }); + + test("uses the first real README paragraph when the about text is empty", () => { + expect(readmeLead(["", "ok", "This is the project introduction."])).toBe("This is the project introduction."); + expect(readmeLead(["字".repeat(900)]).endsWith("…")).toBe(true); + expect(readmeLead(["字".repeat(900)]).length).toBe(801); + }); + + test("builds a card from the repository payload and leaves out live counters", () => { + const facts = githubRepoFactsFromSource(source({ + pageUrl: "https://github.com/Tianma-IF/EdgeEver/tree/main/apps", + openGraphDescription: "Open-source, AI-native knowledge base. - tianma-if/edgeever", + embeddedJsonChunks: [sidebarPayload()], + language: "TypeScript 62.1%", + topics: ["notes", "sqlite"], + })); + expect(facts).toMatchObject({ + owner: "tianma-if", + name: "edgeever", + canonicalUrl: "https://github.com/tianma-if/edgeever", + intro: "Open-source, AI-native knowledge base.", + homepage: "https://edgeever.org/", + language: "TypeScript", + license: "AGPL-3.0", + topics: ["notes", "mcp", "sqlite"], + }); + const markdown = githubRepoNoteMarkdown({ ...facts, ...labels }); + expect(githubRepoNoteTitle(facts.owner, facts.name)).toBe("tianma-if/edgeever"); + expect(markdown.startsWith("Open-source, AI-native knowledge base.")).toBe(true); + expect(markdown).toContain("主页: [edgeever.org](https://edgeever.org/)"); + expect(markdown).toContain("语言: TypeScript"); + expect(markdown).toContain("许可证: AGPL-3.0"); + expect(markdown).toContain("话题: notes, mcp, sqlite"); + expect(markdown).toContain("[https://github.com/tianma-if/edgeever](https://github.com/tianma-if/edgeever)"); + expect(markdown).toContain("抓取时间: 2026-10-01T00:00:00.000Z"); + expect(markdown).not.toContain("1915"); + expect(markdown).not.toContain("995"); + }); + + test("reads a code tree that only identifies the repository, then uses the README lead", () => { + const facts = githubRepoFactsFromSource(source({ + pageUrl: "https://github.com/tianma-if/edgeever/tree/main", + openGraphDescription: "Contribute to tianma-if/edgeever development by creating an account on GitHub.", + embeddedJsonChunks: [JSON.stringify({ + payload: { + codeViewLayoutRoute: { + repo: { ownerLogin: "tianma-if", name: "edgeever", defaultBranch: "main" }, + }, + }, + })], + readmeParagraphs: ["Go", "A portable notes workspace for long-term writing."], + })); + expect(facts?.intro).toBe("A portable notes workspace for long-term writing."); + expect(facts?.homepage).toBe(""); + expect(facts?.topics).toEqual([]); + const markdown = githubRepoNoteMarkdown({ ...facts, ...labels }); + expect(markdown).not.toContain("主页:"); + expect(markdown).not.toContain("话题:"); + expect(markdown).toContain("来源: [https://github.com/tianma-if/edgeever](https://github.com/tianma-if/edgeever)"); + }); + + test("does not treat an unrelated GitHub page as the repository in the address", () => { + expect(githubRepoFactsFromSource(source({ + pageUrl: "https://github.com/features/copilot", + openGraphDescription: "AI pair programmer.", + embeddedJsonChunks: [sidebarPayload()], + }))).toBeNull(); + expect(githubRepoFactsFromSource(source({ + pageUrl: "https://github.com/tianma-if/edgeever/issues/8", + embeddedJsonChunks: [sidebarPayload()], + }))).toBeNull(); + }); +}); diff --git a/apps/extension/options.html b/apps/extension/options.html index 73ca5ff5..f0dd83b0 100644 --- a/apps/extension/options.html +++ b/apps/extension/options.html @@ -11,6 +11,7 @@

Right-click an image on a webpage and choose “Save image to EdgeEver”. It is saved into the default notebook selected below.

Select text on a page, right-click, and choose “Save selection to EdgeEver”. It is saved as a new note in the default notebook selected below.

On X, right-click a post and choose “Save this post to EdgeEver”. The visible text and photos go into one new note in the same notebook.

+

On a GitHub repository page or code tree, right-click the page background, the description, or the README text and choose “Save this repository to EdgeEver”. The address and description are saved as a new note in the default notebook selected above.