From 101ea2f79442c338ec9fa8824e5dc386b2c03feb Mon Sep 17 00:00:00 2001 From: Thaumy Date: Thu, 13 Jul 2023 18:19:25 +0800 Subject: [PATCH 1/7] fix: fix web img extract btn not work --- src/commands/extract-images.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/commands/extract-images.ts b/src/commands/extract-images.ts index 9ed02fc9..043cabbd 100644 --- a/src/commands/extract-images.ts +++ b/src/commands/extract-images.ts @@ -12,18 +12,21 @@ const extractOptions: readonly ExtractOption[] = [ export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | undefined) { if (!(arg instanceof Uri && arg.scheme === 'file')) return - const shouldIgnoreWarnings = inputImageSrc != null + const editor = window.visibleTextEditors.find(x => x.document.fileName === arg.fsPath) + const textDocument = editor?.document ?? workspace.textDocuments.find(x => x.fileName === arg.fsPath) + + if (!textDocument) return + await textDocument.save() + const markdown = (await workspace.fs.readFile(arg)).toString() const extractor = new MarkdownImagesExtractor(markdown, arg) + const images = extractor.findImages() + if (images.length <= 0) + void (!inputImageSrc != null ? window.showWarningMessage('没有找到可以提取的图片') : undefined) + const availableWebImagesCount = images.filter(newImageSrcFilter(ImageSrc.web)).length const availableLocalImagesCount = images.filter(newImageSrcFilter(ImageSrc.local)).length - - const warnNoImages = () => - void (!shouldIgnoreWarnings ? window.showWarningMessage('没有找到可以提取的图片') : undefined) - - if (images.length <= 0) return warnNoImages() - const result = extractOptions.find(x => inputImageSrc != null && x.imageSrc === inputImageSrc) ?? (await window.showInformationMessage( @@ -37,16 +40,10 @@ export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | unde ...extractOptions )) - const editor = window.visibleTextEditors.find(x => x.document.fileName === arg.fsPath) - const textDocument = editor?.document ?? workspace.textDocuments.find(x => x.fileName === arg.fsPath) + if (!(result && result.imageSrc !== undefined)) return - if (!(result && result.imageSrc && textDocument)) return - - if (extractor.findImages().length <= 0) return warnNoImages() extractor.imageSrc = result.imageSrc - await textDocument.save() - const failedImages = await window.withProgress( { title: '提取图片', location: ProgressLocation.Notification }, async progress => { From 2adb755ee77aaa9c0a905a6b24afb572e9270db9 Mon Sep 17 00:00:00 2001 From: Thaumy Date: Fri, 14 Jul 2023 16:41:44 +0800 Subject: [PATCH 2/7] feat: extract data url image --- src/commands/extract-images.ts | 87 ++++---- src/services/image.service.ts | 12 +- src/services/images-extractor.service.ts | 184 ---------------- src/services/mkd-img-extractor.service.ts | 245 ++++++++++++++++++++++ src/services/settings.service.ts | 10 +- 5 files changed, 308 insertions(+), 230 deletions(-) delete mode 100644 src/services/images-extractor.service.ts create mode 100644 src/services/mkd-img-extractor.service.ts diff --git a/src/commands/extract-images.ts b/src/commands/extract-images.ts index 043cabbd..48f68280 100644 --- a/src/commands/extract-images.ts +++ b/src/commands/extract-images.ts @@ -1,15 +1,9 @@ import { MessageItem, MessageOptions, ProgressLocation, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode' -import { ImageSrc, MarkdownImagesExtractor, ImageInfo, newImageSrcFilter } from '@/services/images-extractor.service' +import { ImageInfo, ImageSrc, MkdImgExtractor, newImageSrcFilter } from '@/services/mkd-img-extractor.service' type ExtractOption = MessageItem & Partial<{ imageSrc: ImageSrc }> -const extractOptions: readonly ExtractOption[] = [ - { title: '提取本地图片', imageSrc: ImageSrc.local }, - { title: '提取网络图片', imageSrc: ImageSrc.web }, - { title: '提取全部', imageSrc: ImageSrc.any }, - { title: '取消', imageSrc: undefined, isCloseAffordance: true }, -] - -export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | undefined) { + +export async function extractImages(arg: unknown, inputImageSrc?: ImageSrc) { if (!(arg instanceof Uri && arg.scheme === 'file')) return const editor = window.visibleTextEditors.find(x => x.document.fileName === arg.fsPath) @@ -19,40 +13,58 @@ export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | unde await textDocument.save() const markdown = (await workspace.fs.readFile(arg)).toString() - const extractor = new MarkdownImagesExtractor(markdown, arg) + const extractor = new MkdImgExtractor(markdown, arg) const images = extractor.findImages() if (images.length <= 0) void (!inputImageSrc != null ? window.showWarningMessage('没有找到可以提取的图片') : undefined) - const availableWebImagesCount = images.filter(newImageSrcFilter(ImageSrc.web)).length - const availableLocalImagesCount = images.filter(newImageSrcFilter(ImageSrc.local)).length - const result = - extractOptions.find(x => inputImageSrc != null && x.imageSrc === inputImageSrc) ?? - (await window.showInformationMessage( + const getExtractOption = () => { + const webImgCount = images.filter(newImageSrcFilter(ImageSrc.web)).length + const dataUrlImgCount = images.filter(newImageSrcFilter(ImageSrc.dataUrl)).length + const fsImgCount = images.filter(newImageSrcFilter(ImageSrc.fs)).length + + const displayOptions: ExtractOption[] = [ + { title: '提取全部', imageSrc: ImageSrc.any }, + { title: '提取网络图片', imageSrc: ImageSrc.web }, + { title: '提取 Data Url 图片', imageSrc: ImageSrc.dataUrl }, + { title: '提取本地图片', imageSrc: ImageSrc.fs }, + { title: '取消', imageSrc: undefined, isCloseAffordance: true }, + ] + + if (inputImageSrc !== undefined) + return Promise.resolve(displayOptions.find(ent => ent.imageSrc === inputImageSrc)) + + // if src is not specified: + return window.showInformationMessage( '要提取哪些图片? 此操作会替换源文件中的图片链接!', { modal: true, detail: - `共找到 ${availableWebImagesCount} 张可以提取的网络图片\n` + - `${availableLocalImagesCount} 张可以提取的本地图片`, + '共找到:\n' + + `${webImgCount} 张可以提取的网络图片\n` + + `${dataUrlImgCount} 张可以提取的 Data Url 图片\n` + + `${fsImgCount} 张可以提取的本地图片`, } as MessageOptions, - ...extractOptions - )) + ...displayOptions + ) + } + + const extractImageSrc = (await getExtractOption())?.imageSrc - if (!(result && result.imageSrc !== undefined)) return + if (extractImageSrc === undefined) return - extractor.imageSrc = result.imageSrc + extractor.imageSrc = extractImageSrc const failedImages = await window.withProgress( - { title: '提取图片', location: ProgressLocation.Notification }, + { title: '正在提取图片', location: ProgressLocation.Notification }, async progress => { - extractor.onProgress = (idx, images) => { - const total = images.length - const image = images[idx] + extractor.onProgress = (count, info) => { + const total = info.length + const image = info[count] progress.report({ - increment: (idx / total) * 80, - message: `[${idx + 1} / ${total}] 正在提取 ${image.link}`, + increment: (count / total) * 80, + message: `[${count + 1} / ${total}] 正在提取 ${image.data}`, }) } @@ -65,12 +77,13 @@ export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | unde // eslint-disable-next-line @typescript-eslint/no-non-null-assertion .map(([src, dst]) => [src, dst!]) .map(([src, dst]) => { - const startPos = textDocument.positionAt(src.startOffset) - const endPos = textDocument.positionAt( - src.startOffset + src.prefix.length + src.link.length + src.postfix.length + const posL = textDocument.positionAt(src.startOffset) + const posR = textDocument.positionAt( + src.startOffset + src.prefix.length + src.data.length + src.postfix.length ) - const range = new Range(startPos, endPos) + const range = new Range(posL, posR) + // just for ts type inferring const ret: [Range, ImageInfo] = [range, dst] return ret }) @@ -78,12 +91,12 @@ export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | unde if (range) { progress.report({ increment: (idx / extractedLen) * 20 + 80, - message: `[${idx + 1} / ${extractedLen}] 正在替换图片链接 ${dst.link}`, + message: `[${idx + 1} / ${extractedLen}] 正在替换图片链接 ${dst.data}`, }) - const newText = dst.prefix + dst.link + dst.postfix + const newText = dst.prefix + dst.data + dst.postfix we.replace(textDocument.uri, range, newText, { needsConfirmation: false, - label: dst.link, + label: dst.data, }) } @@ -96,10 +109,10 @@ export async function extractImages(arg: unknown, inputImageSrc: ImageSrc | unde } ) - if (failedImages && failedImages.length > 0) { + if (failedImages.length > 0) { const info = failedImages - .map(x => [x.link, extractor.errors.find(([link]) => link === x.link)?.[1] ?? ''].join(',')) + .map(info => [info.data, extractor.errors.find(([link]) => link === info.data)?.[1] ?? ''].join(',')) .join('\n') - window.showErrorMessage(`${failedImages.length}张图片提取失败: ${info}`).then(undefined, console.warn) + window.showErrorMessage(`${failedImages.length} 张图片提取失败: ${info}`).then(undefined, console.warn) } } diff --git a/src/services/image.service.ts b/src/services/image.service.ts index d0bb7f13..b332ffe4 100644 --- a/src/services/image.service.ts +++ b/src/services/image.service.ts @@ -7,18 +7,20 @@ import path from 'path' class ImageService { async upload( file: T - ): Promise { + ) { // eslint-disable-next-line @typescript-eslint/naming-convention - const FormData = (await import('form-data')).default - const form = new FormData() const { name, fileName, filename, path: _path } = file const finalName = path.basename(isString(_path) ? _path : fileName || filename || name || 'image.png') const ext = path.extname(finalName) + const mime = await import('mime') const mimeType = mime.lookup(ext, 'image/png') - form.append('image', file, { filename: finalName, contentType: mimeType }) + + const fd = new (await import('form-data')).default() + fd.append('image', file, { filename: finalName, contentType: mimeType }) + const response = await httpClient.post(`${globalContext.config.apiBaseUrl}/api/posts/body/images`, { - body: form, + body: fd, }) return response.body diff --git a/src/services/images-extractor.service.ts b/src/services/images-extractor.service.ts deleted file mode 100644 index 6f5611fb..00000000 --- a/src/services/images-extractor.service.ts +++ /dev/null @@ -1,184 +0,0 @@ -import path from 'path' -import fs from 'fs' -import { Uri, workspace } from 'vscode' -import { imageService } from './image.service' -import { isErrorResponse } from '@/models/error-response' -import { isString } from 'lodash-es' -import { promisify } from 'util' -import { Readable } from 'stream' - -export interface ImageInfo { - startOffset: number - prefix: string - link: string - postfix: string -} - -const imgTagImgRegExp = /()/gi -const mkdImgRegExp = /(!\[.*?\]\()(.*?\.(?:png|jpg|jpeg|webp|svg|gif))(\))/gi -const cnblogsDomainRegExp = /\.cnblogs\.com\//gi - -export const enum ImageSrc { - web, - local, - any, -} - -export const newImageSrcFilter = (type: ImageSrc) => { - const isWebImage = (imgInfo: ImageInfo) => /https?:\/\//.test(imgInfo.link) - const isLocalImage = (imgInfo: ImageInfo) => !isWebImage(imgInfo) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const isAnyImage = (_: ImageInfo) => true - - switch (type) { - case ImageSrc.web: - return isWebImage - case ImageSrc.local: - return isLocalImage - case ImageSrc.any: - return isAnyImage - } -} - -enum ExtractorSt { - pending, - extracting, - extracted, -} - -export class MarkdownImagesExtractor { - private _imageSrc = ImageSrc.any - private _status = ExtractorSt.pending - private _errors: [imageLink: string, msg: string][] = [] - private _images: ImageInfo[] | null | undefined = null - private readonly _workspaceDirs: string[] | undefined - - constructor( - private readonly markdown: string, - private readonly targetFileUri: Uri, - public onProgress?: (index: number, images: ImageInfo[]) => void - ) { - this._workspaceDirs = workspace.workspaceFolders?.map(({ uri: { fsPath } }) => fsPath) - } - - get imageSrc() { - return this._imageSrc - } - - set imageSrc(v) { - this._imageSrc = v - } - - get status() { - return this._status - } - - get errors() { - return this._errors - } - - async extract(): Promise<[src: ImageInfo, dst: ImageInfo | null][]> { - this._status = ExtractorSt.extracting - - const srcInfoArr = this.findImages() - let count = 0 - - const result: ReturnType extends Promise ? U : never = [] - - for (const srcInfo of srcInfoArr) { - if (this.onProgress) this.onProgress(count++, srcInfoArr) - - // reuse resolved link - const resolvedLink = result.find(([src, dst]) => dst != null && src.link === srcInfo.link)?.[1]?.link - - const stream = resolvedLink ?? (await this.resolveImageFile(srcInfo)) - - const dstInfo = await (async () => { - if (stream == null) return null - - try { - const newLink = isString(stream) ? stream : await imageService.upload(stream) - - return { - ...srcInfo, - link: newLink, - } - } catch (e) { - const errMsg = `上传图片失败, ${isErrorResponse(e) ? e.errors.join(',') : JSON.stringify(e)}` - this._errors.push([srcInfo.link, errMsg]) - return null - } - })() - - result.push([srcInfo, dstInfo]) - } - - this._status = ExtractorSt.extracted - - return result - } - - findImages(): ImageInfo[] { - const mkdImgMatchGroup = Array.from(this.markdown.matchAll(mkdImgRegExp)) - const imgTagImgMatchGroup = Array.from(this.markdown.matchAll(imgTagImgRegExp)) - const matchGroupAcc = mkdImgMatchGroup.concat(imgTagImgMatchGroup) - - this._images ??= matchGroupAcc - .map(mg => ({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - startOffset: mg.index!, - prefix: mg[1], - link: mg[2], - postfix: mg[3], - })) - .filter(x => !cnblogsDomainRegExp.test(x.link)) - - return this._images.filter(x => newImageSrcFilter(this._imageSrc)(x)) - } - - private async resolveImageFile(info: ImageInfo): Promise { - // for web img - if (newImageSrcFilter(ImageSrc.web)(info)) { - const stream = await imageService.download(info.link) - // TODO: fix warning here - if (stream instanceof Array) { - this._errors.push([info.link, `无法下载网络图片, ${stream[0]} - ${stream[2]}`]) - return - } - return stream - } - - // for local img - const checkReadAccess = (filePath: string) => - promisify(fs.access)(filePath).then( - () => true, - () => false - ) - - let iPath: string | undefined | null = info.link - let iDir = 0 - let searchingDirs: string[] | undefined | null - let triedPath: string[] | undefined - let isEncodedPath = false - - while (iPath != null) { - if (await checkReadAccess(iPath)) { - return fs.createReadStream(iPath) - } else { - triedPath ??= [] - triedPath.push(iPath) - - if (!isEncodedPath) { - iPath = decodeURIComponent(iPath) - isEncodedPath = true - continue - } - } - - searchingDirs ??= [path.dirname(this.targetFileUri.fsPath), ...(this._workspaceDirs ?? [])] - iPath = iDir >= 0 && searchingDirs.length > iDir ? path.resolve(searchingDirs[iDir], info.link) : undefined - iDir++ - isEncodedPath = false - } - } -} diff --git a/src/services/mkd-img-extractor.service.ts b/src/services/mkd-img-extractor.service.ts new file mode 100644 index 00000000..e91b61aa --- /dev/null +++ b/src/services/mkd-img-extractor.service.ts @@ -0,0 +1,245 @@ +import path from 'path' +import { isString, merge, pick } from 'lodash-es' +import fs from 'fs' +import { Uri, workspace } from 'vscode' +import { imageService } from './image.service' +import { isErrorResponse } from '@/models/error-response' +import { promisify } from 'util' +import { Readable } from 'stream' +import httpClient from '@/utils/http-client' +import { tmpdir } from 'os' + +export const enum DataType { + dataUrl, + url, +} + +export interface ImageInfo { + startOffset: number + data: string + dataType: DataType + prefix: string + postfix: string +} + +// Data URL reference see in: +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs +// Related RFC: +// https://datatracker.ietf.org/doc/html/rfc2397 + +const imgTagDataUrlImgPat = /()/g +const imgTagUrlImgPat = /()/gi +const mkdDataUrlImgPat = /(!\[.*?\]\()(data:image\/.*?,[a-zA-Z0-9+/]*?=?=?)(\))/g +const mkdUrlImgPat = /(!\[.*?\]\()(.*?\.(?:png|jpg|jpeg|webp|svg|gif))(\))/gi + +const cnblogsDomainRegExp = /\.cnblogs\.com\//gi + +export const enum ImageSrc { + web, + dataUrl, + fs, + any, +} + +export const newImageSrcFilter = (type: ImageSrc) => { + const isWebImage = (imgInfo: ImageInfo) => imgInfo.dataType === DataType.url && /https?:\/\//.test(imgInfo.data) + const isFsImage = (imgInfo: ImageInfo) => imgInfo.dataType === DataType.url && !isWebImage(imgInfo) + const isDataUrlImage = (imgInfo: ImageInfo) => imgInfo.dataType === DataType.dataUrl + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const isAnyImage = (_: ImageInfo) => true + + switch (type) { + case ImageSrc.web: + return isWebImage + case ImageSrc.fs: + return isFsImage + case ImageSrc.dataUrl: + return isDataUrlImage + case ImageSrc.any: + return isAnyImage + } +} + +enum ExtractorSt { + pending, + extracting, + extracted, +} + +export class MkdImgExtractor { + private _imageSrc = ImageSrc.any + private _status = ExtractorSt.pending + private _errors: [imageLink: string, msg: string][] = [] + private _images: ImageInfo[] | null | undefined = null + private readonly _workspaceDirs: string[] | undefined + + constructor( + private readonly markdown: string, + private readonly targetFileUri: Uri, + public onProgress?: (index: number, images: ImageInfo[]) => void + ) { + this._workspaceDirs = workspace.workspaceFolders?.map(({ uri: { fsPath } }) => fsPath) + } + + get imageSrc() { + return this._imageSrc + } + + set imageSrc(v) { + this._imageSrc = v + } + + get status() { + return this._status + } + + get errors() { + return this._errors + } + + async extract(): Promise<[src: ImageInfo, dst: ImageInfo | null][]> { + this._status = ExtractorSt.extracting + + const srcInfoArr = this.findImages() + let count = 0 + + const result: ReturnType extends Promise ? U : never = [] + + for (const srcInfo of srcInfoArr) { + if (this.onProgress) this.onProgress(count++, srcInfoArr) + + // reuse resolved link + const resolvedLink = result.find(([src, dst]) => dst != null && src.data === srcInfo.data)?.[1]?.data + + const streamOrLink = resolvedLink ?? (await this.resolveImgInfo(srcInfo)) + + const dstInfo = await (async () => { + if (streamOrLink === undefined) return null + try { + const newLink = isString(streamOrLink) ? streamOrLink : await imageService.upload(streamOrLink) + + return { + ...srcInfo, + data: newLink, + } + } catch (e) { + const errMsg = `上传图片失败, ${isErrorResponse(e) ? e.errors.join(',') : JSON.stringify(e)}` + this._errors.push([srcInfo.data, errMsg]) + return null + } + })() + + result.push([srcInfo, dstInfo]) + } + + this._status = ExtractorSt.extracted + + return result + } + + findImages(): ImageInfo[] { + const acc = () => { + const imgTagUrlImgMatchGroups = Array.from(this.markdown.matchAll(imgTagUrlImgPat)) + const mkdUrlImgMatchGroups = Array.from(this.markdown.matchAll(mkdUrlImgPat)) + const urlImgInfo = imgTagUrlImgMatchGroups.concat(mkdUrlImgMatchGroups).map(mg => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + startOffset: mg.index!, + dataType: DataType.url, + data: mg[2], + prefix: mg[1], + postfix: mg[3], + })) + + const imgTagDataUrlImgMatchGroups = Array.from(this.markdown.matchAll(imgTagDataUrlImgPat)) + const mkdDataUrlImgMatchGroups = Array.from(this.markdown.matchAll(mkdDataUrlImgPat)) + const dataUrlImgInfo = imgTagDataUrlImgMatchGroups.concat(mkdDataUrlImgMatchGroups).map(mg => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + startOffset: mg.index!, + dataType: DataType.dataUrl, + data: mg[2], + prefix: mg[1], + postfix: mg[3], + })) + + const acc = urlImgInfo.concat(dataUrlImgInfo) + + // TODO: better filter design needed + // remove cnblogs img link + return acc.filter(x => !cnblogsDomainRegExp.test(x.data)) + } + + this._images ??= acc() + + // apply settings + return this._images.filter(x => newImageSrcFilter(this._imageSrc)(x)) + } + + private async resolveWebImg(url: string) { + try { + return await imageService.download(url) + } catch (e) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + this._errors.push([url, `无法下载网络图片: ${e}`]) + return + } + } + + private async resolveFsImg(imgPath: string) { + const checkReadAccess = (filePath: string) => + promisify(fs.access)(filePath).then( + () => true, + () => false + ) + + let iPath: string | undefined | null = imgPath + let iDir = 0 + let searchingDirs: string[] | undefined | null + let isEncodedPath = false + + while (iPath != null) { + if (await checkReadAccess(iPath)) return fs.createReadStream(iPath) + + if (!isEncodedPath) { + iPath = decodeURIComponent(iPath) + isEncodedPath = true + continue + } + + searchingDirs ??= [path.dirname(this.targetFileUri.fsPath), ...(this._workspaceDirs ?? [])] + iPath = iDir >= 0 && searchingDirs.length > iDir ? path.resolve(searchingDirs[iDir], imgPath) : undefined + iDir++ + isEncodedPath = false + } + } + + private resolveDataUrlImg(dataUrl: string) { + // reference for this impl: + // https://stackoverflow.com/questions/6850276/how-to-convert-dataurl-to-file-object-in-javascript/7261048#7261048 + + const regex = /data:image\/(.*?);.*?,([a-zA-Z0-9+/]*=?=?)/g + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mg = Array.from(dataUrl.matchAll(regex)) + const buf = Buffer.from(mg[0][2], 'base64') + + const ext = mg[0][1] + const fileName = `${Date.now()}.${ext}` + const path = `${tmpdir()}/` + fileName + fs.writeFileSync(path, buf, 'utf8') + + return fs.createReadStream(path) + } + + private async resolveImgInfo(info: ImageInfo): Promise { + // for web img + // eslint-disable-next-line no-return-await + if (newImageSrcFilter(ImageSrc.web)(info)) return await this.resolveWebImg(info.data) + + // for fs img + // eslint-disable-next-line no-return-await + if (newImageSrcFilter(ImageSrc.fs)(info)) return await this.resolveFsImg(info.data) + + // for data url img + if (newImageSrcFilter(ImageSrc.dataUrl)(info)) return this.resolveDataUrlImg(info.data) + } +} diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index b3a71bf7..54056d61 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -1,7 +1,7 @@ import os, { homedir } from 'os' import fs from 'fs' import { ConfigurationTarget, Uri, workspace } from 'vscode' -import { ImageSrc, MarkdownImagesExtractor } from './images-extractor.service' +import { ImageSrc, MkdImgExtractor } from './mkd-img-extractor.service' import { isNumber } from 'lodash-es' import { untildify } from '@/utils/untildify' @@ -74,11 +74,13 @@ export class Settings { } static get automaticallyExtractImagesType(): ImageSrc | null { - const cfg = this.configuration.get<'disable' | 'web' | 'local' | 'any'>('automaticallyExtractImages') ?? null + const cfg = + this.configuration.get<'disable' | 'web' | 'dataUrl' | 'fs' | 'any'>('automaticallyExtractImages') ?? null - if (cfg === 'local') return ImageSrc.local - if (cfg === 'any') return ImageSrc.any + if (cfg === 'fs') return ImageSrc.fs + if (cfg === 'dataUrl') return ImageSrc.dataUrl if (cfg === 'web') return ImageSrc.web + if (cfg === 'any') return ImageSrc.any return null // 'disable' case } From edd4af201518b5f9cea23308ee436e4175d8dbd0 Mon Sep 17 00:00:00 2001 From: Thaumy Date: Mon, 17 Jul 2023 11:11:52 +0800 Subject: [PATCH 3/7] fix: make img tag self-closing pattern optional --- src/services/mkd-img-extractor.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/mkd-img-extractor.service.ts b/src/services/mkd-img-extractor.service.ts index e91b61aa..3cf59cd4 100644 --- a/src/services/mkd-img-extractor.service.ts +++ b/src/services/mkd-img-extractor.service.ts @@ -27,8 +27,8 @@ export interface ImageInfo { // Related RFC: // https://datatracker.ietf.org/doc/html/rfc2397 -const imgTagDataUrlImgPat = /()/g -const imgTagUrlImgPat = /()/gi +const imgTagDataUrlImgPat = /()/g +const imgTagUrlImgPat = /()/gi const mkdDataUrlImgPat = /(!\[.*?\]\()(data:image\/.*?,[a-zA-Z0-9+/]*?=?=?)(\))/g const mkdUrlImgPat = /(!\[.*?\]\()(.*?\.(?:png|jpg|jpeg|webp|svg|gif))(\))/gi From 2c43e285ffbe9a8bf1d2913972b631f13b0f7a4d Mon Sep 17 00:00:00 2001 From: Thaumy Date: Wed, 19 Jul 2023 17:33:06 +0800 Subject: [PATCH 4/7] fix: update setting option --- package.json | 6 ++++-- src/commands/posts-list/save-post.ts | 8 ++++---- src/services/settings.service.ts | 21 +++++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 1ddb2978..76151b32 100644 --- a/package.json +++ b/package.json @@ -466,24 +466,26 @@ "type": "boolean", "markdownDescription": "创建本地博文时, 是否根据博文分类保存到对应的文件夹中" }, - "cnblogsClientForVSCode.automaticallyExtractImages": { + "cnblogsClientForVSCode.autoExtractImages": { "order": 6, "default": "disable", "scope": "application", "enum": [ "disable", "local", + "dataurl", "web", "any" ], "enumItemLabels": [ "禁用", "自动提取本地图片", + "自动提取由 Base64 编码的图片", "自动提取网络图片", "自动提取全部图片" ], "editPresentation": "singlelineText", - "markdownDescription": "提取图片, 配置保存到博客园时要自动提取上传到博客园的图片" + "markdownDescription": "提取图片, 配置上传到博客园时要自动提取上传到博客园的图片" }, "cnblogsClientForVSCode.pageSize.postsList": { "order": 7, diff --git a/src/commands/posts-list/save-post.ts b/src/commands/posts-list/save-post.ts index 03aa741d..b1dbad28 100644 --- a/src/commands/posts-list/save-post.ts +++ b/src/commands/posts-list/save-post.ts @@ -115,8 +115,8 @@ export const saveLocalDraftToCnblogs = async (localDraft: LocalDraft) => { AlertService.warning('本地文件已删除, 无法新建博文') return false } - if (Settings.automaticallyExtractImagesType) - await extractImages(localDraft.filePathUri, Settings.automaticallyExtractImagesType).catch(console.warn) + if (Settings.autoExtractImgType) + await extractImages(localDraft.filePathUri, Settings.autoExtractImgType).catch(console.warn) postToSave.postBody = await localDraft.readAllText() return true @@ -138,8 +138,8 @@ export const savePostToCnblogs = async (input: Post | PostTreeItem | PostEditDto const localFilePath = PostFileMapManager.getFilePath(postId) if (!localFilePath) return AlertService.warning('本地无该博文的编辑记录') - if (Settings.automaticallyExtractImagesType) - await extractImages(Uri.file(localFilePath), Settings.automaticallyExtractImagesType).catch(console.warn) + if (Settings.autoExtractImgType) + await extractImages(Uri.file(localFilePath), Settings.autoExtractImgType).catch(console.warn) await saveFilePendingChanges(localFilePath) post.postBody = (await workspace.fs.readFile(Uri.file(localFilePath))).toString() diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index 54056d61..d646eee9 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -1,13 +1,12 @@ import os, { homedir } from 'os' import fs from 'fs' import { ConfigurationTarget, Uri, workspace } from 'vscode' -import { ImageSrc, MkdImgExtractor } from './mkd-img-extractor.service' +import { ImageSrc } from './mkd-img-extractor.service' import { isNumber } from 'lodash-es' import { untildify } from '@/utils/untildify' export class Settings { static readonly postsListPageSizeKey = 'pageSize.postsList' - static readonly platform = os.platform() static readonly prefix = `cnblogsClientForVSCode` static readonly iconThemePrefix = 'workbench' static readonly iconThemeKey = 'iconTheme' @@ -18,8 +17,7 @@ export class Settings { private static _adaptLegacyWorkspaceTask?: Thenable | null static get platformPrefix() { - const { platform } = this - switch (platform) { + switch (os.platform()) { case 'darwin': return 'macos' case 'win32': @@ -41,12 +39,16 @@ export class Settings { static get platformConfiguration() { const { platformPrefix, prefix } = this - return platformPrefix ? workspace.getConfiguration(`${prefix}.${platformPrefix}`) : null + + if (platformPrefix) return workspace.getConfiguration(`${prefix}.${platformPrefix}`) + + return null } static get workspaceUri(): Uri { - if (this.legacyWorkspaceUri != null) { - const legacy = this.legacyWorkspaceUri + const legacy = this.legacyWorkspaceUri + + if (legacy != null) { if (this._adaptLegacyWorkspaceTask == null) { try { this._adaptLegacyWorkspaceTask = this.removeLegacyWorkspaceUri().then( @@ -73,9 +75,8 @@ export class Settings { return this.configuration.get('createLocalPostFileWithCategory') ?? false } - static get automaticallyExtractImagesType(): ImageSrc | null { - const cfg = - this.configuration.get<'disable' | 'web' | 'dataUrl' | 'fs' | 'any'>('automaticallyExtractImages') ?? null + static get autoExtractImgType(): ImageSrc | null { + const cfg = this.configuration.get<'disable' | 'web' | 'dataUrl' | 'fs' | 'any'>('autoExtractImages') ?? null if (cfg === 'fs') return ImageSrc.fs if (cfg === 'dataUrl') return ImageSrc.dataUrl From 4e1d0f1fea7d3558fa06b140f67fd4822bd62f70 Mon Sep 17 00:00:00 2001 From: Thaumy Date: Thu, 20 Jul 2023 10:20:31 +0800 Subject: [PATCH 5/7] fix: fix auto extract img --- package.json | 4 ++-- src/commands/extract-images.ts | 2 +- src/commands/posts-list/save-post.ts | 9 +++++++-- src/services/settings.service.ts | 14 ++++++++------ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 76151b32..699272cd 100644 --- a/package.json +++ b/package.json @@ -472,8 +472,8 @@ "scope": "application", "enum": [ "disable", - "local", - "dataurl", + "fs", + "dataUrl", "web", "any" ], diff --git a/src/commands/extract-images.ts b/src/commands/extract-images.ts index 48f68280..eb4cce98 100644 --- a/src/commands/extract-images.ts +++ b/src/commands/extract-images.ts @@ -17,7 +17,7 @@ export async function extractImages(arg: unknown, inputImageSrc?: ImageSrc) { const images = extractor.findImages() if (images.length <= 0) - void (!inputImageSrc != null ? window.showWarningMessage('没有找到可以提取的图片') : undefined) + void (!inputImageSrc !== undefined ? window.showWarningMessage('没有找到可以提取的图片') : undefined) const getExtractOption = () => { const webImgCount = images.filter(newImageSrcFilter(ImageSrc.web)).length diff --git a/src/commands/posts-list/save-post.ts b/src/commands/posts-list/save-post.ts index b1dbad28..3f4a4e97 100644 --- a/src/commands/posts-list/save-post.ts +++ b/src/commands/posts-list/save-post.ts @@ -115,7 +115,10 @@ export const saveLocalDraftToCnblogs = async (localDraft: LocalDraft) => { AlertService.warning('本地文件已删除, 无法新建博文') return false } - if (Settings.autoExtractImgType) + + console.log(Settings.autoExtractImgType) + + if (Settings.autoExtractImgType !== undefined) await extractImages(localDraft.filePathUri, Settings.autoExtractImgType).catch(console.warn) postToSave.postBody = await localDraft.readAllText() @@ -138,7 +141,9 @@ export const savePostToCnblogs = async (input: Post | PostTreeItem | PostEditDto const localFilePath = PostFileMapManager.getFilePath(postId) if (!localFilePath) return AlertService.warning('本地无该博文的编辑记录') - if (Settings.autoExtractImgType) + console.log(Settings.autoExtractImgType) + + if (Settings.autoExtractImgType !== undefined) await extractImages(Uri.file(localFilePath), Settings.autoExtractImgType).catch(console.warn) await saveFilePendingChanges(localFilePath) diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index d646eee9..74c495f6 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -75,15 +75,17 @@ export class Settings { return this.configuration.get('createLocalPostFileWithCategory') ?? false } - static get autoExtractImgType(): ImageSrc | null { - const cfg = this.configuration.get<'disable' | 'web' | 'dataUrl' | 'fs' | 'any'>('autoExtractImages') ?? null + static get autoExtractImgType(): ImageSrc | undefined { + const cfg = this.configuration.get<'disable' | 'web' | 'dataUrl' | 'fs' | 'any'>('autoExtractImages') - if (cfg === 'fs') return ImageSrc.fs - if (cfg === 'dataUrl') return ImageSrc.dataUrl + console.log(this.configuration) + console.log(cfg) + + if (cfg === 'disable') return if (cfg === 'web') return ImageSrc.web + if (cfg === 'dataUrl') return ImageSrc.dataUrl + if (cfg === 'fs') return ImageSrc.fs if (cfg === 'any') return ImageSrc.any - - return null // 'disable' case } static get postsListPageSize() { From 0909318f0cb1f98422dacc48b81698b4124852e4 Mon Sep 17 00:00:00 2001 From: Thaumy Date: Wed, 26 Jul 2023 11:31:16 +0800 Subject: [PATCH 6/7] chore: update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 35d6eb0e..47984115 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules .DS_Store .idea .vscode/api-key.txt +with-key-build.sh From fc16977cea980e04e04e597b4cfdda8af4d80e98 Mon Sep 17 00:00:00 2001 From: Thaumy Date: Wed, 26 Jul 2023 17:12:26 +0800 Subject: [PATCH 7/7] fix: dup extract img --- src/commands/extract-images.ts | 4 +++- src/services/mkd-img-extractor.service.ts | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/commands/extract-images.ts b/src/commands/extract-images.ts index eb4cce98..e1a76bbb 100644 --- a/src/commands/extract-images.ts +++ b/src/commands/extract-images.ts @@ -16,8 +16,10 @@ export async function extractImages(arg: unknown, inputImageSrc?: ImageSrc) { const extractor = new MkdImgExtractor(markdown, arg) const images = extractor.findImages() - if (images.length <= 0) + if (images.length <= 0) { void (!inputImageSrc !== undefined ? window.showWarningMessage('没有找到可以提取的图片') : undefined) + return + } const getExtractOption = () => { const webImgCount = images.filter(newImageSrcFilter(ImageSrc.web)).length diff --git a/src/services/mkd-img-extractor.service.ts b/src/services/mkd-img-extractor.service.ts index 3cf59cd4..c4a2e9f0 100644 --- a/src/services/mkd-img-extractor.service.ts +++ b/src/services/mkd-img-extractor.service.ts @@ -29,8 +29,8 @@ export interface ImageInfo { const imgTagDataUrlImgPat = /()/g const imgTagUrlImgPat = /()/gi -const mkdDataUrlImgPat = /(!\[.*?\]\()(data:image\/.*?,[a-zA-Z0-9+/]*?=?=?)(\))/g -const mkdUrlImgPat = /(!\[.*?\]\()(.*?\.(?:png|jpg|jpeg|webp|svg|gif))(\))/gi +const mkdDataUrlImgPat = /(!\[.*?]\()(data:image\/.*?,[a-zA-Z0-9+/]*?=?=?)(\))/g +const mkdUrlImgPat = /(!\[.*?]\()(.*?\.(?:png|jpg|jpeg|webp|svg|gif))(\))/gi const cnblogsDomainRegExp = /\.cnblogs\.com\//gi @@ -163,12 +163,13 @@ export class MkdImgExtractor { const acc = urlImgInfo.concat(dataUrlImgInfo) + //console.log(k) // TODO: better filter design needed // remove cnblogs img link - return acc.filter(x => !cnblogsDomainRegExp.test(x.data)) + return acc.filter(x => x.data.match(cnblogsDomainRegExp) == null) } - this._images ??= acc() + this._images = acc() // apply settings return this._images.filter(x => newImageSrcFilter(this._imageSrc)(x))