Initial commit: Image to HTML plugin with i18n support

This commit is contained in:
Ke Yan 2025-03-08 22:44:05 +08:00
commit 96d248d7fd
12 changed files with 717 additions and 0 deletions

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# Intellij
*.iml
.idea
# npm
node_modules
package-lock.json
# build
main.js
*.js.map
# obsidian
data.json
# macOS
.DS_Store
# test
/test-vault

85
README.md Normal file
View file

@ -0,0 +1,85 @@
# Obsidian Image to HTML
[中文文档](README_CN.md)
A plugin for Obsidian that converts pasted images to HTML format instead of Obsidian's wikilink or Markdown format.
## Features
- Saves pasted images as files and inserts them as HTML image tags
- Supports custom image width
- Supports custom image save paths (e.g., `./assets`)
- Optional alt attribute for better accessibility
- Optional paste notifications
## Why HTML Format?
HTML image tags offer better compatibility across different platforms compared to Markdown or Obsidian's wikilink format. This is particularly useful for users who need to share their notes across different platforms and Markdown editors.
## Usage
1. Install the plugin in Obsidian
2. Copy an image (from browser, file explorer, or screenshot tool)
3. Paste in the Obsidian editor
4. The image will be saved and inserted as HTML: `<img src="image_name.jpg" width="auto">`
### Custom Image Path
You can enable the "Use Custom Image Path" option in settings and set a custom save path for your images:
- Relative paths (e.g., `./assets` or `../images`): Images will be saved in the specified directory relative to the current file
- Absolute paths (e.g., `assets` or `images`): Images will be saved in the specified directory under your vault root
When custom path is enabled, the src attribute in HTML tags will be updated accordingly, for example:
```html
<img src="./assets/image_1234567890.jpg" width="auto">
```
If custom path is disabled, images will be saved in the same directory as the current file, and the HTML tag will only use the filename:
```html
<img src="image_1234567890.jpg" width="auto">
```
### About Alt Attribute
By default, the plugin generates HTML tags without the alt attribute to keep the code concise. If you need better accessibility or SEO optimization, you can enable the "Include Alt Attribute" option in settings, which will generate tags like:
```html
<img src="image_1234567890.jpg" width="auto" alt="image_1234567890.jpg">
```
## Settings
- **Image Width**: Set the image width, can be pixels (e.g., 500px), percentage (e.g., 100%), or auto
- **Use Custom Image Path**: When enabled, images will be saved to the specified path instead of the current file's directory
- **Custom Image Path**: Set the image save path, can be relative (e.g., ./assets) or absolute (e.g., assets)
- **Include Alt Attribute**: When enabled, HTML image tags will include the alt attribute for better accessibility and SEO
- **Show Notifications**: Choose whether to show notifications when pasting images
## Installation
### From Obsidian Community Plugins
1. Open Obsidian Settings
2. Go to "Community Plugins"
3. Disable "Safe Mode"
4. Click "Browse"
5. Search for "Image to HTML"
6. Click Install
7. Enable the plugin
### Manual Installation
1. Download the latest version of `main.js`, `manifest.json`, and `styles.css`
2. Copy these files to your vault's `.obsidian/plugins/img2html/` directory
## Development
1. Clone this repository
2. Run `npm install` to install dependencies
3. Run `npm run dev` to start compilation in watch mode
4. Copy `main.js`, `manifest.json`, and `styles.css` to your vault's `.obsidian/plugins/img2html/` directory for testing
## License
MIT

85
README_CN.md Normal file
View file

@ -0,0 +1,85 @@
# Obsidian Image to HTML
[English](README.md)
这是一个 Obsidian 插件,用于将粘贴的图片转换为 HTML 格式,而不是 Obsidian 的 wikilink 或 Markdown 格式。
## 功能
- 将粘贴的图片保存为文件并插入 HTML 格式的图片标签
- 支持自定义图片宽度
- 支持自定义图片保存路径(如 `./assets`
- 可选择是否包含 alt 属性
- 粘贴时可选择显示通知
## 为什么使用 HTML 格式?
HTML 格式的图片标签比 Markdown 或 Obsidian 的 wikilink 格式更广泛兼容,可以被大多数 Markdown 编辑器识别。这对于需要在不同平台之间共享笔记的用户特别有用。
## 使用方法
1. 在 Obsidian 中安装此插件
2. 复制一张图片(从浏览器、文件资源管理器或截图工具)
3. 在 Obsidian 编辑器中粘贴
4. 图片将被保存并以 HTML 格式插入:`<img src="image_name.jpg" width="auto">`
### 自定义图片路径
您可以在设置中启用"使用自定义图片路径"选项,并设置自定义的图片保存路径:
- 相对路径(如 `./assets``../images`):图片将保存在相对于当前文件的指定目录中
- 绝对路径(如 `assets``images`):图片将保存在 vault 根目录下的指定目录中
启用自定义路径后HTML 图片标签中的 src 属性也会相应更新,例如:
```html
<img src="./assets/image_1234567890.jpg" width="auto">
```
如果不启用自定义路径,图片将保存在当前文件所在的目录中,并且 HTML 标签中只使用文件名:
```html
<img src="image_1234567890.jpg" width="auto">
```
### 关于 alt 属性
默认情况下,插件生成的 HTML 标签不包含 alt 属性,以保持代码简洁。如果您需要更好的无障碍性或 SEO 优化,可以在设置中启用"包含 alt 属性"选项,这将生成如下格式的标签:
```html
<img src="image_1234567890.jpg" width="auto" alt="image_1234567890.jpg">
```
## 设置
- **图片宽度**:设置图片的宽度,可以是像素值(如 500px、百分比如 100%)或 auto
- **使用自定义图片路径**:启用后,图片将保存到指定路径而不是当前文件所在目录
- **自定义图片路径**:设置图片保存的路径,可以是相对路径(如 ./assets或绝对路径如 assets
- **包含 alt 属性**启用后HTML 图片标签将包含 alt 属性,有助于无障碍性和 SEO但会使标签更长
- **显示通知**:粘贴图片时是否显示通知
## 安装
### 从 Obsidian 社区插件中安装
1. 打开 Obsidian 设置
2. 转到"第三方插件"
3. 禁用"安全模式"
4. 点击"浏览"
5. 搜索"Image to HTML"
6. 点击安装
7. 启用插件
### 手动安装
1. 下载最新版本的 `main.js`、`manifest.json` 和 `styles.css`
2. 将这些文件复制到你的 vault 的 `.obsidian/plugins/img2html/` 目录下
## 开发
1. 克隆此仓库
2. 运行 `npm install` 安装依赖
3. 运行 `npm run dev` 开始开发,这将自动编译并监视文件变化
4. 将 `main.js`、`manifest.json` 和 `styles.css` 复制到你的 vault 的 `.obsidian/plugins/img2html/` 目录下进行测试
## 许可证
MIT

48
esbuild.config.mjs Normal file
View file

@ -0,0 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
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');
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',
...builtins],
format: 'cjs',
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
});
if (prod) {
await context.rebuild();
await context.dispose();
} else {
await context.watch();
}

285
main.ts Normal file
View file

@ -0,0 +1,285 @@
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile, normalizePath } from 'obsidian';
import { getTranslations, Translations } from './src/i18n';
interface Img2HtmlSettings {
// 图片宽度设置,可以是固定值或百分比
imageWidth: string;
// 是否在粘贴时显示通知
showNotice: boolean;
// 自定义图片保存路径
imagePath: string;
// 是否使用自定义路径
useCustomPath: boolean;
// 是否包含 alt 属性
includeAlt: boolean;
}
const DEFAULT_SETTINGS: Img2HtmlSettings = {
imageWidth: 'auto',
showNotice: true,
imagePath: './assets',
useCustomPath: false,
includeAlt: false
}
export default class Img2HtmlPlugin extends Plugin {
settings: Img2HtmlSettings;
i18n: Translations;
statusBarItemEl: HTMLElement;
settingTab: Img2HtmlSettingTab;
async onload() {
await this.loadSettings();
// 加载国际化翻译
this.i18n = getTranslations();
// 注册编辑器粘贴事件
this.registerEvent(
this.app.workspace.on('editor-paste', this.handlePaste.bind(this))
);
// 添加设置选项卡
this.settingTab = new Img2HtmlSettingTab(this.app, this);
this.addSettingTab(this.settingTab);
// 添加状态栏项目
this.statusBarItemEl = this.addStatusBarItem();
this.statusBarItemEl.setText(this.i18n.statusBar.enabled);
// 监听语言变更
this.registerDomEvent(window, 'storage', (e: StorageEvent) => {
if (e.key === 'language') {
this.updateI18n();
}
});
}
onunload() {
// 插件卸载时的清理工作
}
/**
*
*/
updateI18n() {
// 更新翻译
this.i18n = getTranslations();
// 更新状态栏
this.statusBarItemEl.setText(this.i18n.statusBar.enabled);
// 如果设置页面已打开,则刷新设置页面
if (this.settingTab && this.settingTab.containerEl.parentElement) {
this.settingTab.display();
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
/**
*
*/
async handlePaste(evt: ClipboardEvent, editor: Editor, view: MarkdownView) {
// 如果剪贴板中没有文件,则不处理
if (!evt.clipboardData?.files.length) {
return;
}
// 阻止默认粘贴行为
evt.preventDefault();
const file = evt.clipboardData.files[0];
// 检查是否为图片文件
if (!file.type.startsWith('image/')) {
return;
}
// 获取当前文件
const activeFile = view.file;
if (!activeFile) {
return;
}
// 生成图片文件名
const timestamp = new Date().getTime();
const fileName = `image_${timestamp}.${this.getFileExtension(file.type)}`;
// 创建图片文件路径
let imagePath = '';
let imageDir = '';
if (this.settings.useCustomPath) {
// 使用自定义路径
const basePath = activeFile.parent?.path || '';
const customPath = this.settings.imagePath.trim();
// 确保自定义路径不以斜杠开头
const normalizedCustomPath = customPath.startsWith('/')
? customPath.substring(1)
: customPath;
// 如果是相对路径(以 ./ 或 ../ 开头)
if (normalizedCustomPath.startsWith('./') || normalizedCustomPath.startsWith('../')) {
// 直接使用相对路径
imageDir = normalizePath(`${basePath}/${normalizedCustomPath}`);
} else {
// 否则视为从 vault 根目录开始的路径
imageDir = normalizePath(normalizedCustomPath);
}
// 确保目录存在
if (!await this.app.vault.adapter.exists(imageDir)) {
await this.app.vault.createFolder(imageDir);
}
imagePath = `${imageDir}/${fileName}`;
} else {
// 使用默认路径(与当前文件同目录)
const basePath = activeFile.parent?.path || '';
imageDir = basePath;
imagePath = `${basePath}/${fileName}`;
}
// 读取图片文件内容
const buffer = await file.arrayBuffer();
// 保存图片到 vault
await this.app.vault.createBinary(imagePath, buffer);
// 构建 HTML 图片标签
const imgTag = this.createHtmlImgTag(fileName, imagePath, imageDir);
// 插入 HTML 图片标签到编辑器
editor.replaceSelection(imgTag);
// 显示通知
if (this.settings.showNotice) {
new Notice(`${this.i18n.notice.imagePasted} ${imageDir}`);
}
}
/**
* MIME
*/
getFileExtension(mimeType: string): string {
const mimeToExt: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/svg+xml': 'svg',
'image/webp': 'webp',
'image/bmp': 'bmp',
'image/tiff': 'tiff'
};
return mimeToExt[mimeType] || 'png';
}
/**
* HTML
*/
createHtmlImgTag(fileName: string, imagePath: string, imageDir: string): string {
let src = '';
if (this.settings.useCustomPath) {
// 使用自定义路径
const customPath = this.settings.imagePath.trim();
// 如果是相对路径(以 ./ 或 ../ 开头)
if (customPath.startsWith('./') || customPath.startsWith('../')) {
src = `${customPath}/${fileName}`;
} else {
// 否则添加 ./ 前缀
src = `./${customPath}/${fileName}`.replace(/\/\//g, '/');
}
} else {
// 使用默认路径(与当前文件同目录)
src = fileName;
}
// 构建 HTML 标签
if (this.settings.includeAlt) {
return `<img src="${src}" width="${this.settings.imageWidth}" alt="${fileName}">`;
} else {
return `<img src="${src}" width="${this.settings.imageWidth}">`;
}
}
}
class Img2HtmlSettingTab extends PluginSettingTab {
plugin: Img2HtmlPlugin;
constructor(app: App, plugin: Img2HtmlPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
const i18n = this.plugin.i18n;
containerEl.empty();
containerEl.createEl('h2', {text: i18n.settings.title});
new Setting(containerEl)
.setName(i18n.settings.imageWidth.name)
.setDesc(i18n.settings.imageWidth.desc)
.addText(text => text
.setPlaceholder('auto')
.setValue(this.plugin.settings.imageWidth)
.onChange(async (value) => {
this.plugin.settings.imageWidth = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(i18n.settings.useCustomPath.name)
.setDesc(i18n.settings.useCustomPath.desc)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useCustomPath)
.onChange(async (value) => {
this.plugin.settings.useCustomPath = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(i18n.settings.imagePath.name)
.setDesc(i18n.settings.imagePath.desc)
.addText(text => text
.setPlaceholder('./assets')
.setValue(this.plugin.settings.imagePath)
.onChange(async (value) => {
this.plugin.settings.imagePath = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(i18n.settings.includeAlt.name)
.setDesc(i18n.settings.includeAlt.desc)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeAlt)
.onChange(async (value) => {
this.plugin.settings.includeAlt = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(i18n.settings.showNotice.name)
.setDesc(i18n.settings.showNotice.desc)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNotice)
.onChange(async (value) => {
this.plugin.settings.showNotice = value;
await this.plugin.saveSettings();
}));
}
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "img2html",
"name": "Image to HTML",
"version": "0.1.0",
"minAppVersion": "0.1.0",
"description": "Paste images as HTML format instead of Obsidian wikilink or markdown format",
"author": "0x1DA9430",
"authorUrl": "https://github.com/0x1DA9430",
"isDesktopOnly": false
}

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "img2html",
"version": "1.0.0",
"description": "Paste images as HTML format instead of Obsidian wikilink or markdown format",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"plugin",
"image",
"html"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "^0.25.0",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

108
src/i18n.ts Normal file
View file

@ -0,0 +1,108 @@
export interface Translations {
settings: {
title: string;
imageWidth: {
name: string;
desc: string;
};
useCustomPath: {
name: string;
desc: string;
};
imagePath: {
name: string;
desc: string;
};
includeAlt: {
name: string;
desc: string;
};
showNotice: {
name: string;
desc: string;
};
};
statusBar: {
enabled: string;
};
notice: {
imagePasted: string;
};
}
const zh: Translations = {
settings: {
title: 'Image to HTML 设置',
imageWidth: {
name: '图片宽度',
desc: '设置粘贴图片的宽度,可以是像素值(如 500px或百分比如 100%)或 auto'
},
useCustomPath: {
name: '使用自定义图片路径',
desc: '启用后,图片将保存到指定路径而不是当前文件所在目录'
},
imagePath: {
name: '自定义图片路径',
desc: '设置图片保存的路径,可以是相对路径(如 ./assets或绝对路径如 assets'
},
includeAlt: {
name: '包含 alt 属性',
desc: '启用后HTML 图片标签将包含 alt 属性,有助于无障碍性和 SEO但会使标签更长'
},
showNotice: {
name: '显示通知',
desc: '粘贴图片时显示通知'
}
},
statusBar: {
enabled: 'Img2Html: 开启'
},
notice: {
imagePasted: '图片已粘贴为 HTML 格式,保存在'
}
};
const en: Translations = {
settings: {
title: 'Image to HTML Settings',
imageWidth: {
name: 'Image Width',
desc: 'Set the width of pasted images, can be a pixel value (e.g. 500px), percentage (e.g. 100%) or auto'
},
useCustomPath: {
name: 'Use Custom Image Path',
desc: 'When enabled, images will be saved to the specified path instead of the current file directory'
},
imagePath: {
name: 'Custom Image Path',
desc: 'Set the path to save images, can be a relative path (e.g. ./assets) or absolute path (e.g. assets)'
},
includeAlt: {
name: 'Include alt Attribute',
desc: 'When enabled, HTML image tags will include the alt attribute, which helps with accessibility and SEO, but makes the tag longer'
},
showNotice: {
name: 'Show Notification',
desc: 'Show notification when pasting images'
}
},
statusBar: {
enabled: 'Img2Html: Enabled'
},
notice: {
imagePasted: 'Image pasted as HTML format, saved in'
}
};
export function getTranslations(): Translations {
// 获取 Obsidian 当前语言设置
const lang = window.localStorage.getItem('language');
// 如果是中文,返回中文翻译
if (lang === 'zh' || lang === 'zh-TW') {
return zh;
}
// 默认返回英文翻译
return en;
}

0
styles.css Normal file
View file

24
tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}

20
version-bump.mjs Normal file
View file

@ -0,0 +1,20 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.argv[2];
const minAppVersion = process.argv[3];
// read minAppVersion from manifest.json if it's not provided
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion: currentMinAppVersion } = manifest;
// update manifest.json
manifest.version = targetVersion;
if (minAppVersion) {
manifest.minAppVersion = minAppVersion;
}
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion || currentMinAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "0.1.0"
}