mirror of
https://github.com/ckelsoe/obsidian-rss-importer.git
synced 2026-07-22 07:48:56 +00:00
Scaffold from standard plugin template
Bootstrap obsidian-rss-importer from plugin-templates/standard: placeholders substituted, isDesktopOnly set true, Turndown + jsdom test deps added, esbuild bumped to 0.28.1 to clear advisory GHSA-gv7w-rqvm-qjhr (matches shell-path-copy). Build and lint green; main.ts is a wiring-only stub pending feature phases.
This commit is contained in:
commit
35ac67a806
27 changed files with 10878 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
61
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
name: Bug report
|
||||
description: Report something that is not working as expected.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug.
|
||||
|
||||
**Before you post:** anything you write or attach here is **public**. Do not include passwords, personal file contents, or other sensitive information. Redact file paths and screenshots as needed. You post this information at your own discretion. See the [privacy policy](https://github.com/ckelsoe/obsidian-rss-importer/blob/main/PRIVACY.md).
|
||||
|
||||
Security vulnerabilities must **not** be filed here. See [SECURITY.md](https://github.com/ckelsoe/obsidian-rss-importer/blob/main/SECURITY.md).
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Describe the bug and what you expected instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: List the exact steps that trigger the bug.
|
||||
placeholder: |
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: obsidian-version
|
||||
attributes:
|
||||
label: Obsidian version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: Plugin version
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
- iOS / iPadOS
|
||||
- Android
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: confirm
|
||||
attributes:
|
||||
label: Confirmation
|
||||
options:
|
||||
- label: I have removed any sensitive information from this report.
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Security vulnerability (do not file publicly)
|
||||
url: https://github.com/ckelsoe/obsidian-rss-importer/blob/main/SECURITY.md
|
||||
about: Report security issues privately. Do not open a public issue.
|
||||
- name: Privacy policy
|
||||
url: https://github.com/ckelsoe/obsidian-rss-importer/blob/main/PRIVACY.md
|
||||
about: What data the plugin does and does not handle.
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
name: Feature request
|
||||
description: Suggest an idea or improvement.
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the suggestion.
|
||||
|
||||
**Note:** anything you write here is **public**. Do not include sensitive information.
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What problem would this solve?
|
||||
description: Describe the use case or workflow gap.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe what you would like the plugin to do.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Other approaches or workarounds you have tried.
|
||||
validations:
|
||||
required: false
|
||||
138
.github/workflows/ci.yml
vendored
Normal file
138
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
# Weekly OSV re-scan so new advisories surface even when no code changes
|
||||
- cron: "0 12 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npx tsc --noEmit --skipLibCheck
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Validate Obsidian manifest
|
||||
run: |
|
||||
echo "Validating manifest.json..."
|
||||
# Check required fields exist
|
||||
for field in id name version minAppVersion description author; do
|
||||
if ! jq -e ".$field" manifest.json > /dev/null 2>&1; then
|
||||
echo "::error::Missing required field: $field"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Check version format (semver)
|
||||
VERSION=$(jq -r '.version' manifest.json)
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$ ]]; then
|
||||
echo "::error::Invalid version format: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
# Check versions.json exists and contains current version
|
||||
if [ -f versions.json ]; then
|
||||
if ! jq -e ".[\"$VERSION\"]" versions.json > /dev/null 2>&1; then
|
||||
echo "::warning::Version $VERSION not found in versions.json"
|
||||
fi
|
||||
fi
|
||||
echo "Manifest validation passed"
|
||||
|
||||
- name: Check for deprecated Obsidian API usage
|
||||
run: |
|
||||
echo "Scanning for known deprecated Obsidian APIs..."
|
||||
FOUND=0
|
||||
|
||||
# workspace.activeLeaf — deprecated, use getActiveViewOfType()
|
||||
if grep -n "\.activeLeaf\b" main.ts; then
|
||||
echo "::error::Deprecated API: 'activeLeaf'. Use getActiveViewOfType() or getLeaf() instead."
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# MarkdownRenderer.renderMarkdown — deprecated, use MarkdownRenderer.render()
|
||||
if grep -n "renderMarkdown(" main.ts; then
|
||||
echo "::error::Deprecated API: 'renderMarkdown'. Use MarkdownRenderer.render() instead."
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# getLeaf() with a boolean argument — deprecated, use getLeaf('tab') etc.
|
||||
if grep -n "getLeaf(true)\|getLeaf(false)" main.ts; then
|
||||
echo "::error::Deprecated API: 'getLeaf(bool)'. Use getLeaf('tab') or getLeaf('split') instead."
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# Notice.noticeEl — deprecated, use Notice.messageEl
|
||||
if grep -n "\.noticeEl\b" main.ts; then
|
||||
echo "::error::Deprecated API: 'noticeEl'. Use 'messageEl' instead."
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
if [ "$FOUND" -eq 0 ]; then
|
||||
echo "No deprecated Obsidian API usage found."
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check bundle size
|
||||
run: |
|
||||
SIZE=$(stat -c%s main.js 2>/dev/null || stat -f%z main.js)
|
||||
echo "Bundle size: $SIZE bytes ($(echo "scale=2; $SIZE/1024" | bc) KB)"
|
||||
if [ "$SIZE" -gt 500000 ]; then
|
||||
echo "::warning::Bundle size exceeds 500KB"
|
||||
fi
|
||||
|
||||
osv-scan:
|
||||
name: OSV Scanner
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Run OSV-Scanner
|
||||
uses: google/osv-scanner-action/osv-scanner-action@v2.0.3
|
||||
with:
|
||||
scan-args: |-
|
||||
--lockfile=package-lock.json
|
||||
--recursive
|
||||
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: high
|
||||
comment-summary-in-pr: on-failure
|
||||
93
.github/workflows/release.yml
vendored
Normal file
93
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: rss-importer
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
- name: VirusTotal scan of release artifacts
|
||||
# Best-effort: VirusTotal returns HTTP 409 ("resource already exists")
|
||||
# when a byte-identical artifact was scanned before, and the action
|
||||
# treats that as fatal. continue-on-error keeps a 409 (or any VirusTotal
|
||||
# hiccup) from blocking the release; the scan still runs and reports.
|
||||
continue-on-error: true
|
||||
uses: crazy-max/ghaction-virustotal@v5
|
||||
with:
|
||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
||||
files: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
- name: Extract release notes from CHANGELOG
|
||||
id: notes
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
awk -v ver="$tag" '
|
||||
$0 ~ "^## \\[" ver "\\]" { in_section=1; next }
|
||||
in_section && /^## \[/ { exit }
|
||||
in_section { print }
|
||||
' CHANGELOG.md > release-notes.md
|
||||
echo "Extracted release notes:"
|
||||
cat release-notes.md
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
# Mark pre-release tags (-beta/-rc/-alpha) so Obsidian's marketplace
|
||||
# updater and stable BRAT installs do not pick them up as the latest.
|
||||
prerelease=""
|
||||
case "$tag" in
|
||||
*-beta*|*-rc*|*-alpha*) prerelease="--prerelease" ;;
|
||||
esac
|
||||
if [ -s release-notes.md ]; then
|
||||
gh release create "$tag" $prerelease \
|
||||
--title="$tag" \
|
||||
--notes-file release-notes.md \
|
||||
main.js manifest.json styles.css
|
||||
else
|
||||
gh release create "$tag" $prerelease \
|
||||
--title="$tag" \
|
||||
--generate-notes \
|
||||
main.js manifest.json styles.css
|
||||
fi
|
||||
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
# AI assistant files
|
||||
.claude
|
||||
CLAUDE.md
|
||||
WARP.md
|
||||
|
||||
# Local Obsidian workspace
|
||||
.obsidian/
|
||||
|
||||
# Screenshots and images (add specific ones to repo if needed)
|
||||
Selection_*.png
|
||||
*.screenshot.png
|
||||
|
||||
# Backup files
|
||||
main_backup.ts
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# Review files
|
||||
GeminiReview.md
|
||||
*Review.md
|
||||
|
||||
# Temporary files
|
||||
*.zip
|
||||
*.tmp
|
||||
|
||||
# Internal documentation
|
||||
internal-docs/
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
tag-version-prefix=""
|
||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - YYYY-MM-DD
|
||||
|
||||
### Added
|
||||
- Initial release.
|
||||
69
CONTRIBUTING.md
Normal file
69
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Contributing to RSS Importer
|
||||
|
||||
Thanks for your interest in improving RSS Importer. This guide explains how to get a local build running, propose changes, and submit pull requests.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
- Search [existing issues](https://github.com/ckelsoe/obsidian-rss-importer/issues) before opening a new one.
|
||||
- For bugs, include Obsidian version, platform (Windows/macOS/Linux/iOS/Android), plugin version, and reproduction steps.
|
||||
- For feature requests, describe the use case and how it fits the plugin's scope.
|
||||
|
||||
## Development setup
|
||||
|
||||
1. Fork and clone the repo.
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. Start the dev build with file watching:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
4. Copy `main.js`, `manifest.json`, and `styles.css` into a test vault under `.obsidian/plugins/rss-importer/`, then enable the plugin in Obsidian.
|
||||
|
||||
## Quality gates
|
||||
|
||||
Before opening a pull request, run:
|
||||
|
||||
```bash
|
||||
npm run lint # ESLint with eslint-plugin-obsidianmd recommended, zero warnings allowed
|
||||
npm run build # TypeScript strict type-check + esbuild production bundle
|
||||
npm test # Jest unit tests
|
||||
```
|
||||
|
||||
All three must pass. Pull requests that break CI will not be reviewed until green.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
- TypeScript strict mode is on. Never use `as any` casting. If types are missing, add declarations to `types.d.ts`.
|
||||
- Follow [Obsidian's plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Prefer `containerEl.createDiv()` / `createSpan()` over generic `createEl('div'|'span')`.
|
||||
- Do not import Node built-ins (`path`, `fs`, etc.) at the top of `main.ts`. Use a `Platform.isDesktop`-guarded `require()` instead.
|
||||
- No inline `style` attributes. Move styles to `styles.css`.
|
||||
- Settings tab headings must avoid the words "settings", "options", "general", and the plugin name.
|
||||
- All UI strings (commands, menu titles, setting names, notifications) use sentence case. Brands recognized by `eslint-plugin-obsidianmd` (Markdown, macOS, iOS, Windows, Linux, etc.) keep their official casing.
|
||||
- Keep the settings UI compatible with mobile (`isDesktopOnly: false` means features must degrade gracefully on iOS/Android).
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. Create a feature branch off `main`.
|
||||
2. Make focused, atomic commits.
|
||||
3. Update `CHANGELOG.md` under an `## [Unreleased]` heading describing user-visible changes.
|
||||
4. Push your branch and open a PR against `main`.
|
||||
5. Fill out the PR description: what changed, why, and how to test it.
|
||||
|
||||
## Releases
|
||||
|
||||
Releases are cut by the maintainer. The release workflow runs automatically on tag push and:
|
||||
|
||||
1. Builds and tests the plugin.
|
||||
2. Generates SLSA build provenance attestation for `main.js`, `manifest.json`, and `styles.css`.
|
||||
3. Submits artifacts to VirusTotal for malware analysis (requires `VT_API_KEY` repo secret).
|
||||
4. Extracts the matching `CHANGELOG.md` section as release notes.
|
||||
5. Publishes the GitHub release.
|
||||
|
||||
Contributors do not need to bump versions or create release artifacts.
|
||||
|
||||
## License
|
||||
|
||||
By contributing you agree that your contributions are licensed under the MIT License (see `LICENSE`).
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Charles Kelsoe (ckelsoe)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS 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.
|
||||
48
PRIVACY.md
Normal file
48
PRIVACY.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Privacy Policy
|
||||
|
||||
_Last updated: 2026-06-14_
|
||||
|
||||
This policy explains what the **RSS Importer** Obsidian plugin ("the plugin") does and does not do with your data. It applies to the plugin as distributed through the Obsidian Community Plugins marketplace, GitHub releases, and BRAT.
|
||||
|
||||
## Summary
|
||||
|
||||
The plugin collects nothing, stores nothing outside your own vault, and sends nothing anywhere. It contains no network code.
|
||||
|
||||
## What the plugin does
|
||||
|
||||
_Describe the plugin's data flow in plain terms here. Be specific about what files the plugin reads, what files it modifies, when it modifies them, and whether the modifications are triggered by an explicit user action._
|
||||
|
||||
## Data collection
|
||||
|
||||
- **No personal data is collected.** The plugin does not collect names, email addresses, file contents, usage statistics, or any other information.
|
||||
- **No telemetry or analytics.** There is no tracking, crash reporting, or phone-home behavior of any kind.
|
||||
- **No automatic background activity.** The plugin acts only when you explicitly invoke a command or trigger a documented event.
|
||||
|
||||
## Data storage
|
||||
|
||||
- The plugin's settings are stored by Obsidian in your vault's local `data.json` file, on your own device. They never leave your device.
|
||||
- Any other data the plugin reads or writes stays inside your vault, on your own device.
|
||||
|
||||
## Network use
|
||||
|
||||
The plugin contains **no network code**. It makes no HTTP requests, opens no sockets, and contacts no servers. Not the maintainer's, not Obsidian's, not any third party.
|
||||
|
||||
## Third parties
|
||||
|
||||
The plugin shares no data with any third party. It has no data to share and no means to transmit it.
|
||||
|
||||
## Disclaimer of liability
|
||||
|
||||
The plugin is provided free of charge, "AS IS", without warranty of any kind, as set out in the [MIT License](./LICENSE). To the maximum extent permitted by law, the maintainer is not liable for any loss, damage, or claim arising from use of the plugin.
|
||||
|
||||
## Information you choose to share
|
||||
|
||||
If you open a GitHub issue, discussion, or pull request, anything you paste there (file contents, screenshots, vault structure, system details) becomes **public**. The maintainer does not request this information and is not responsible for content you choose to post. Review and redact anything sensitive before submitting. To report a security vulnerability privately instead, see [SECURITY.md](./SECURITY.md).
|
||||
|
||||
## Changes to this policy
|
||||
|
||||
This policy may be updated as the plugin evolves. Material changes will be noted in [CHANGELOG.md](./CHANGELOG.md). The "last updated" date above reflects the current version.
|
||||
|
||||
## Contact
|
||||
|
||||
Questions about this policy: open an issue at [github.com/ckelsoe/obsidian-rss-importer/issues](https://github.com/ckelsoe/obsidian-rss-importer/issues). Do not use a public issue for security vulnerabilities; see [SECURITY.md](./SECURITY.md) for the private reporting channel.
|
||||
40
README.md
Normal file
40
README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# RSS Importer
|
||||
|
||||
[](https://github.com/ckelsoe/obsidian-rss-importer/actions/workflows/ci.yml) [](https://github.com/ckelsoe/obsidian-rss-importer/actions/workflows/release.yml) [](https://github.com/ckelsoe/obsidian-rss-importer/releases) [](https://github.com/ckelsoe/obsidian-rss-importer) [](https://obsidian.md) [](https://github.com/ckelsoe/obsidian-rss-importer/blob/main/LICENSE) [](https://github.com/ckelsoe/obsidian-rss-importer/releases/latest)
|
||||
|
||||
Import articles and podcasts from RSS, Atom, and Substack feeds into your vault as Markdown notes, organized by source feed and deduplicated by note identity.
|
||||
|
||||
## Installation
|
||||
|
||||
### From Obsidian Community Plugins (recommended)
|
||||
|
||||
1. Open Obsidian settings.
|
||||
2. Navigate to **Community plugins**.
|
||||
3. Click **Browse**.
|
||||
4. Search for **RSS Importer**.
|
||||
5. Click **Install**, then **Enable**.
|
||||
|
||||
### Manual installation
|
||||
|
||||
1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/ckelsoe/obsidian-rss-importer/releases/latest).
|
||||
2. Create a folder named `rss-importer` in your vault's `.obsidian/plugins/` directory.
|
||||
3. Copy the downloaded files into this folder.
|
||||
4. Reload Obsidian.
|
||||
5. Enable **RSS Importer** in Settings → Community plugins.
|
||||
|
||||
### BRAT (optional, for pre-release testing)
|
||||
|
||||
BRAT lets power users install pre-release builds before they reach the marketplace.
|
||||
|
||||
1. Install the **BRAT** plugin from Community Plugins.
|
||||
2. Open BRAT settings and click **Add Beta Plugin**.
|
||||
3. Enter: `https://github.com/ckelsoe/obsidian-rss-importer`
|
||||
4. Enable **RSS Importer** in Settings → Community plugins.
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, quality gates, and conventions.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](./LICENSE).
|
||||
35
SECURITY.md
Normal file
35
SECURITY.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest published version of RSS Importer receives security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| latest | :white_check_mark: |
|
||||
| older | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in RSS Importer, please report it **privately** so it can be fixed before public disclosure:
|
||||
|
||||
1. **DO NOT** open a public GitHub issue for security vulnerabilities.
|
||||
2. Open the repository's **Security** tab and click **Report a vulnerability**, or use this direct link: <https://github.com/ckelsoe/obsidian-rss-importer/security/advisories/new>
|
||||
3. Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
|
||||
Reports submitted through GitHub private vulnerability reporting are visible only to you and the maintainer until an advisory is published.
|
||||
|
||||
### What to expect:
|
||||
|
||||
- Acknowledgment within 48 hours
|
||||
- Assessment and response within 7 days
|
||||
- Security patch released as soon as possible
|
||||
- Credit given to reporter (unless you prefer to remain anonymous)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
_Describe what the plugin does and does not do that's relevant to security: file reads, file writes, network access (none expected), external commands, etc._
|
||||
97
esbuild.config.mjs
Normal file
97
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
// Auto-copy build artifacts into the workspace test vault so reloading Obsidian
|
||||
// picks up changes without a manual copy step. Skips silently if the test vault
|
||||
// or the plugin's folder under it does not exist (e.g., on CI).
|
||||
const TEST_VAULT_PLUGINS_DIR = path.resolve(
|
||||
process.cwd(),
|
||||
"..",
|
||||
"..",
|
||||
"obs-test-vault",
|
||||
".obsidian",
|
||||
"plugins",
|
||||
);
|
||||
|
||||
const copyToTestVaultPlugin = {
|
||||
name: "copy-to-test-vault",
|
||||
setup(build) {
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) return;
|
||||
try {
|
||||
const manifestText = await fs.readFile(
|
||||
path.join(process.cwd(), "manifest.json"),
|
||||
"utf8",
|
||||
);
|
||||
const manifest = JSON.parse(manifestText);
|
||||
const targetDir = path.join(TEST_VAULT_PLUGINS_DIR, manifest.id);
|
||||
try {
|
||||
await fs.access(targetDir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const f of ["main.js", "manifest.json", "styles.css"]) {
|
||||
try {
|
||||
await fs.copyFile(path.join(process.cwd(), f), path.join(targetDir, f));
|
||||
} catch (e) {
|
||||
if (e.code !== "ENOENT") throw e;
|
||||
}
|
||||
}
|
||||
console.log(`[copy-to-test-vault] Synced to ${targetDir}`);
|
||||
} catch (e) {
|
||||
console.warn(`[copy-to-test-vault] Skipped: ${e.message}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtinModules,
|
||||
...builtinModules.map(m => `node:${m}`)],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
plugins: [copyToTestVaultPlugin],
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
47
eslint.config.mts
Normal file
47
eslint.config.mts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import tseslint from "typescript-eslint";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import globals from "globals";
|
||||
import { globalIgnores } from "eslint/config";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
"eslint.config.mts",
|
||||
"manifest.json",
|
||||
],
|
||||
},
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
extraFileExtensions: [".json"],
|
||||
},
|
||||
},
|
||||
},
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ["__tests__/**/*.ts"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
},
|
||||
globalIgnores([
|
||||
"node_modules",
|
||||
"dist",
|
||||
"main.js",
|
||||
"scripts",
|
||||
"esbuild.config.mjs",
|
||||
"version-bump.mjs",
|
||||
"versions.json",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"tsconfig.json",
|
||||
"jest.config.cjs",
|
||||
]),
|
||||
);
|
||||
13
jest.config.cjs
Normal file
13
jest.config.cjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': ['ts-jest', {
|
||||
tsconfig: {
|
||||
// Override ESNext module to CommonJS for Jest compatibility
|
||||
module: 'CommonJS',
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
10
main.ts
Normal file
10
main.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Plugin } from "obsidian";
|
||||
|
||||
/**
|
||||
* RSS Importer plugin entry point.
|
||||
*
|
||||
* The plugin class is wiring only. Command registration, the settings tab, and
|
||||
* feed-source wiring are added across the build phases; sources and any
|
||||
* network-dependent state are constructed in `app.workspace.onLayoutReady`.
|
||||
*/
|
||||
export default class RssImporterPlugin extends Plugin {}
|
||||
11
manifest.json
Normal file
11
manifest.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "rss-importer",
|
||||
"name": "RSS Importer",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "Import articles and podcasts from RSS, Atom, and Substack feeds into your vault as Markdown notes, organized by source feed and deduplicated by note identity.",
|
||||
"author": "Charles Kelsoe (ckelsoe)",
|
||||
"authorUrl": "https://github.com/ckelsoe",
|
||||
"fundingUrl": "https://buymeacoffee.com/ckelsoe",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
9877
package-lock.json
generated
Normal file
9877
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
47
package.json
Normal file
47
package.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "rss-importer",
|
||||
"version": "0.1.0",
|
||||
"description": "Import articles and podcasts from RSS, Atom, and Substack feeds into your vault as Markdown notes, organized by source feed and deduplicated by note identity.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "eslint . --max-warnings 0 && node scripts/check-submission.mjs",
|
||||
"test": "jest",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Charles Kelsoe (ckelsoe)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"turndown": "^7.2.4",
|
||||
"turndown-plugin-gfm": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.7",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"globals": "^17.1.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.4.1",
|
||||
"jiti": "^2.6.1",
|
||||
"obsidian": "^1.13.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.35.1"
|
||||
},
|
||||
"overrides": {
|
||||
"handlebars": "^4.7.9",
|
||||
"picomatch": "^2.3.2",
|
||||
"flatted": "^3.4.0",
|
||||
"ajv": "^6.14.0",
|
||||
"brace-expansion@1": "^1.1.13",
|
||||
"brace-expansion@2": "^2.0.3"
|
||||
}
|
||||
}
|
||||
97
scripts/check-submission.mjs
Normal file
97
scripts/check-submission.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Pre-submission guard for Obsidian's automated marketplace review.
|
||||
//
|
||||
// Checks a small set of review rules that the local eslint plugin
|
||||
// (eslint-plugin-obsidianmd) does NOT cover and that are not in the prose
|
||||
// developer docs, but ARE enforced by Obsidian's online developer-dashboard
|
||||
// preview scan. This is a PRE-FILTER, not the authoritative gate: the preview
|
||||
// scan is the real gate and must be run on the release commit before publishing.
|
||||
//
|
||||
// Exits non-zero on any finding so it can chain into `npm run lint` and CI.
|
||||
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
|
||||
const findings = [];
|
||||
|
||||
// Forbid suppressing any obsidianmd/* lint rule. Obsidian's developer-dashboard
|
||||
// scan rejects disabling its rules, and local eslint cannot report its own
|
||||
// suppressions (an `// eslint-disable obsidianmd/...` reads as zero local errors
|
||||
// but fails the dashboard), so this guard scans source for them and fails the
|
||||
// build. Comply with the rule (rename, restructure) instead of disabling it.
|
||||
const CODE_EXT = /\.(ts|mts|cts|tsx|js|mjs|cjs)$/;
|
||||
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "scripts"]);
|
||||
// Anchored to a comment opener (// or /*) so prose that merely mentions
|
||||
// "eslint-disable" is not flagged; eslint only honors directives at a comment's start.
|
||||
const DISABLE_OBSIDIANMD = /(?:\/\/|\/\*)\s*eslint-disable(?:-next-line|-line)?[^\n]*\bobsidianmd\//;
|
||||
// Every eslint directive comment must carry a `-- description` (the dashboard
|
||||
// enforces eslint-comments/require-description, which local eslint does not). A
|
||||
// directive line is compliant only if it also contains a `--` separator.
|
||||
const ESLINT_DIRECTIVE = /(?:\/\/|\/\*)\s*eslint-(?:disable|enable)(?:-next-line|-line)?\b/;
|
||||
|
||||
function* walkCode(dir) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = `${dir}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
if (!SKIP_DIRS.has(entry.name)) yield* walkCode(path);
|
||||
} else if (CODE_EXT.test(entry.name) && entry.name !== "main.js") {
|
||||
yield path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of walkCode(".")) {
|
||||
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
||||
lines.forEach((line, index) => {
|
||||
const where = `${file.replace(/^\.\//, "")}:${index + 1}`;
|
||||
if (DISABLE_OBSIDIANMD.test(line)) {
|
||||
findings.push(`${where}: do not eslint-disable an obsidianmd/* rule; comply with it (rename or restructure) instead.`);
|
||||
}
|
||||
if (ESLINT_DIRECTIVE.test(line) && !line.includes("--")) {
|
||||
findings.push(`${where}: eslint directive comment needs a "-- description" explaining why it is necessary.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The manifest description must not contain the word "Obsidian" (the online
|
||||
// review rejects it as redundant with the plugin-directory context), must be at
|
||||
// most 250 characters, and must end with sentence punctuation.
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const description = typeof manifest.description === "string" ? manifest.description : "";
|
||||
if (/\bobsidian\b/i.test(description)) {
|
||||
findings.push('manifest.json: the description must not contain the word "Obsidian".');
|
||||
}
|
||||
if (description.length > 250) {
|
||||
findings.push(`manifest.json: the description is ${description.length} characters; the maximum is 250.`);
|
||||
}
|
||||
if (description && !/[.!?]$/.test(description.trim())) {
|
||||
findings.push("manifest.json: the description must end with '.', '!' or '?'.");
|
||||
}
|
||||
} catch (error) {
|
||||
findings.push(`manifest.json: could not read or parse (${error.message}).`);
|
||||
}
|
||||
|
||||
// styles.css must not use !important (raise selector specificity instead). Block
|
||||
// comments are blanked first so a comment that mentions the token is not flagged,
|
||||
// while line numbers are preserved.
|
||||
try {
|
||||
const css = readFileSync("styles.css", "utf8");
|
||||
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "));
|
||||
withoutComments.split(/\r?\n/).forEach((line, index) => {
|
||||
if (/!important/i.test(line)) {
|
||||
findings.push(`styles.css:${index + 1}: avoid !important; raise selector specificity instead.`);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// styles.css is optional; skip if absent.
|
||||
}
|
||||
|
||||
if (findings.length > 0) {
|
||||
console.error("Submission pre-check failed:");
|
||||
for (const finding of findings) console.error(` - ${finding}`);
|
||||
console.error("");
|
||||
console.error("This is a pre-filter only. Run the Obsidian developer-dashboard preview scan");
|
||||
console.error("on the release commit for the authoritative result.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Submission pre-check passed (pre-filter only; the developer-dashboard preview scan is the authoritative gate).");
|
||||
1
styles.css
Normal file
1
styles.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* RSS Importer styles. Selectors use Obsidian theme variables; no inline styles, no !important. */
|
||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"strictBindCallApply": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"ES2017",
|
||||
"ES2018"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
11
types.d.ts
vendored
Normal file
11
types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Local type augmentations for the Obsidian API.
|
||||
// Add type declarations here when the Obsidian types are missing or need extension.
|
||||
// NEVER use `as any` to work around missing types. Add a proper declaration here instead.
|
||||
|
||||
import 'obsidian';
|
||||
|
||||
declare module 'obsidian' {
|
||||
interface PluginManifest {
|
||||
version: string;
|
||||
}
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"0.1.0": "1.13.0"
|
||||
}
|
||||
Loading…
Reference in a new issue