From c0b1797e271ce160bccdbae807f99739e95398f4 Mon Sep 17 00:00:00 2001 From: Maxime ROBERT Date: Sun, 7 Jul 2019 13:47:36 +0100 Subject: [PATCH] feat: automatically remap local images to remote ones when publishing --- README.md | 6 ++---- package.json | 2 +- src/article.spec.ts | 43 +++++++++++++++++++++++++++++++++++++ src/article.ts | 41 ++++++++++++++++++++++++++++++++++- src/dev-to-git.interface.ts | 13 +++++++++-- src/dev-to-git.ts | 35 ++++++++++++++++++++++++++++-- test/article.md | 6 ++++++ test/dev-to-git.test.ts | 32 --------------------------- 8 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 src/article.spec.ts diff --git a/README.md b/README.md index a887df7..9b0a599 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,9 @@ Simple and from there you have control over the following properties: `title`, ` ## How do I add images to my blog posts? -Instead of uploading them manually on dev.to, simply put them on your git repo and within the markdown use a link like the following example: +Instead of uploading them manually on dev.to, simply put them within your git repo and within the blog post use a relative link. Here's an example: `The following is an image: ![alt text](./assets/image.png 'Title image')`. -https://raw.githubusercontent.com/YOUR-USERNAME/YOUR-REPO/master/blog-posts/name-of-your-blog-post/assets/your-asset.png - -Alternatively you can use a CDN like https://gitcdn.xyz where you'd just need to paste the previous URL and use the one that website gives you. +If you've got some plugin to preview your markdown from your IDE, the images will be correctly displayed. Then, on CI, right before they're published, the link will be updated to match the raw file. ## How to setup CI for auto deploying the blog posts? diff --git a/package.json b/package.json index 86ec932..79f9679 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "Maxime Robert ", "repository": { "type": "git", - "url": "" + "url": "https://github.com/maxime1992/dev-to-git.git" }, "license": "MIT", "engines": { diff --git a/src/article.spec.ts b/src/article.spec.ts new file mode 100644 index 0000000..8fb9a40 --- /dev/null +++ b/src/article.spec.ts @@ -0,0 +1,43 @@ +import { Article } from './article'; +import { Repository } from './dev-to-git.interface'; + +describe(`Article`, () => { + let article: Article; + const repository: Repository = { username: `maxime1992`, name: 'dev-to-git' }; + const relativePathToArticle = `./test/article.md`; + + beforeEach(() => { + article = new Article({ + id: 0, + relativePathToArticle, + repository, + }); + }); + + describe(`Read`, () => { + let articleRead: string; + + beforeEach(() => { + articleRead = article.readArticleOnDisk(); + }); + + it(`should read an article from the configuration`, () => { + expect(articleRead).toContain(`This is my awesome article!`); + expect(articleRead).toContain(`Hey, some text!`); + }); + + it(`should rewrite the local images URLs to match the raw file on github`, () => { + expect(articleRead).toContain( + `Image 1: ![alt text 1](https://raw.githubusercontent.com/${repository.username}/${repository.name}/master/test/image-1.png 'Title image 1')`, + ); + + expect(articleRead).toContain( + `Image 2: ![alt text 2](https://raw.githubusercontent.com/${repository.username}/${repository.name}/master/test/image-2.png 'Title image 2')`, + ); + + expect(articleRead).toContain( + `Image 3: ![alt text 3](https://raw.githubusercontent.com/${repository.username}/${repository.name}/master/test/image-3.png)`, + ); + }); + }); +}); diff --git a/src/article.ts b/src/article.ts index 425a28f..d1f4dbc 100644 --- a/src/article.ts +++ b/src/article.ts @@ -2,11 +2,50 @@ import { ArticleConfig, ArticleApi } from './dev-to-git.interface'; import got from 'got'; import fs from 'fs'; +const imagesRe: RegExp = /\!\[.*\]\(.*\)/g; +const imageRe: RegExp = /\!\[(.*)\]\(([^ \)]*)(?: '(.*)')?\)/; + +const excludeArticleFromPath = (path: string): string => path.replace(/\/[^\/]+\.md$/, ''); + +interface ImageToReplace { + localImage: string; + remoteImage: string; +} + export class Article { constructor(private articleConfig: ArticleConfig) {} + private updateLocalImageLinks(article: string): string { + let searchImageResult, + localImagesToReplace: ImageToReplace[] = []; + + while ((searchImageResult = imagesRe.exec(article))) { + const [image] = searchImageResult; + + const [_, alt = null, path, title = null] = imageRe.exec(image) || [null, null, null, null]; + + if (path) { + const basePath: string = excludeArticleFromPath(this.articleConfig.relativePathToArticle.substr(2)); + const assetPath = path.substr(2); + + localImagesToReplace.push({ + localImage: image, + remoteImage: `![${alt || ''}](https://raw.githubusercontent.com/${this.articleConfig.repository.username}\/${ + this.articleConfig.repository.name + }/master/${basePath}/${assetPath}${title ? ` '${title}'` : ``})`, + }); + } + } + + return localImagesToReplace.reduce( + (articleTemp, imageToReplace) => articleTemp.replace(imageToReplace.localImage, imageToReplace.remoteImage), + article, + ); + } + public readArticleOnDisk(): string { - return fs.readFileSync(this.articleConfig.relativePathToArticle).toString(); + const article = fs.readFileSync(this.articleConfig.relativePathToArticle).toString(); + return this.updateLocalImageLinks(article); } public publishArticle(token: string): got.GotPromise { diff --git a/src/dev-to-git.interface.ts b/src/dev-to-git.interface.ts index bc014b6..e19cab7 100644 --- a/src/dev-to-git.interface.ts +++ b/src/dev-to-git.interface.ts @@ -1,8 +1,17 @@ -export interface ArticleConfig { - id: string; +export type Repository = { + readonly username: string; + readonly name: string; +}; + +export interface ArticleConfigFile { + id: number; relativePathToArticle: string; } +export interface ArticleConfig extends ArticleConfigFile { + repository: Repository; +} + // https://dev.to/api#available-json-parameters export interface ArticleApi { body_markdown: string; diff --git a/src/dev-to-git.ts b/src/dev-to-git.ts index e313ff0..ab4aa73 100644 --- a/src/dev-to-git.ts +++ b/src/dev-to-git.ts @@ -1,14 +1,17 @@ import minimist from 'minimist'; import fs from 'fs'; import dotenv from 'dotenv'; -import { ArticleConfig } from './dev-to-git.interface'; +import { ArticleConfig, ArticleConfigFile, Repository } from './dev-to-git.interface'; import { Article } from './article'; export const DEFAULT_CONFIG_PATH: string = './dev-to-git.json'; +const repositoryRe: RegExp = /.*\/(.*)\/(.*)\.git/; + export class DevToGit { private configPath: string = DEFAULT_CONFIG_PATH; private token: string = ''; + private repository: Repository = { username: '', name: '' }; constructor() { dotenv.config(); @@ -19,6 +22,8 @@ export class DevToGit { this.configPath = config; } + this.extractRepository(); + if (!process.env.DEV_TO_GIT_TOKEN) { throw new Error('Token is required'); } @@ -26,6 +31,25 @@ export class DevToGit { this.token = process.env.DEV_TO_GIT_TOKEN; } + private extractRepository(): void { + try { + const packageJson = JSON.parse(fs.readFileSync('./package.json').toString()); + + const matchRepositoryUrl = (packageJson.repository.url as string).match(repositoryRe); + + if (matchRepositoryUrl) { + const [_, username, name] = matchRepositoryUrl; + this.repository = { username, name }; + } else { + throw new Error(); + } + } catch (error) { + throw new Error( + 'You must have within your "package.json" a "repository" attribute which is an object and contains itself an attribute "url" like the following: https://github-gitlab-whatever.com/username/repository-name.git - this will be used to generate images links if necessary', + ); + } + } + public getConfigPath(): string { return this.configPath; } @@ -33,7 +57,14 @@ export class DevToGit { public readConfigFile(): ArticleConfig[] { // @todo check structure of the object - return JSON.parse(fs.readFileSync(this.getConfigPath()).toString()) as ArticleConfig[]; + const articleConfigFiles: ArticleConfigFile[] = JSON.parse( + fs.readFileSync(this.getConfigPath()).toString(), + ) as ArticleConfigFile[]; + + return articleConfigFiles.map(articleConfigFile => ({ + ...articleConfigFile, + repository: this.repository, + })); } public publishArticles() { diff --git a/test/article.md b/test/article.md index 72eed67..37ddd95 100644 --- a/test/article.md +++ b/test/article.md @@ -10,3 +10,9 @@ canonical_url: # This is my awesome article! Hey, some text! + +Image 1: ![alt text 1](./image-1.png 'Title image 1') + +Image 2: ![alt text 2](./image-2.png 'Title image 2') + +Image 3: ![alt text 3](./image-3.png) diff --git a/test/dev-to-git.test.ts b/test/dev-to-git.test.ts index 4f6119f..6454e6f 100644 --- a/test/dev-to-git.test.ts +++ b/test/dev-to-git.test.ts @@ -37,36 +37,4 @@ describe(`DevToGit`, () => { }); }); }); - - // describe(`Article`, () => { - // describe(`Read`, () => { - // it(`should read an article from the configuration`, () => { - // const CUSTOM_CONFIG_PATH: string = './test/dev-to-git.json' - // process.argv = [ - // 'don-t-care', - // 'don-t-care', - // '--config', - // CUSTOM_CONFIG_PATH - // ] - // const devToGit = new DevToGit() - // expect(devToGit.readArticleOnDisk()).toContain( - // 'This is my awesome article!' - // ) - // expect(devToGit.readArticleOnDisk()).toContain('Hey, some text!') - // }) - // }) - // describe(`Publish`, () => { - // it(`should publish the article`, () => { - // const CUSTOM_CONFIG_PATH: string = './test/dev-to-git.json' - // process.argv = [ - // 'don-t-care', - // 'don-t-care', - // '--config', - // CUSTOM_CONFIG_PATH - // ] - // const devToGit = new DevToGit() - // devToGit.publishArticle(devToGit.readConfigFile()[0]) - // }) - // }) - // }) });