Skip to content

Commit f7a43b4

Browse files
rion18mihaeu
authored andcommitted
Added script support and custom renderer
1 parent 47b71c6 commit f7a43b4

17 files changed

+297
-27
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ jspm_packages
4242
tags
4343

4444
dist
45+
.idea

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.17.0] - 2022-11-08
11+
12+
### Added
13+
14+
- Added capability of running Cosmere as a script library.
15+
- Added support for custom renderers.
16+
1017
## [0.16.0] - 2022-07-12
1118

1219
### Changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Sync your markdown files to confluence.
77
- upload new versions only when necessary
88
- upload/delete local images as attachments
99
- supports the original markdown spec and parts from [CommonMark](https://marked.js.org/#specifications) and [GitHub Flavored Markdown](https://marked.js.org/#specifications)
10+
- can be used as a CLI command or in a script
1011

1112
## Usage
1213

@@ -107,6 +108,34 @@ or create an alias:
107108
}
108109
```
109110

111+
## Using Cosmere as a library
112+
113+
```js
114+
import cosmere from "cosmere/dist/src/lib";
115+
116+
const config = {
117+
"baseUrl": "<your base url including /rest/api>",
118+
"user": "<your username>",
119+
"pass": "<your password>",
120+
"cachePath": "build",
121+
"prefix": "This document is automatically generated. Please don't edit it directly!",
122+
"insecure": false,
123+
"force": false,
124+
"fileRoot": '/usr/bin/myawesomefolder',
125+
"pages": [
126+
{
127+
"pageId": "1234567890",
128+
"file": "README.md", // this path will be evaluated as fileRoot + file
129+
"title": "Optional title in the confluence page, remove to use # h1 from markdown file instead"
130+
}
131+
],
132+
customRenderer: MyNewAwesomeRenderer,
133+
}
134+
135+
await cosmere(config);
136+
137+
```
138+
110139
## Troubleshooting
111140

112141
### Custom certificates on Confluence instance

src/ConfigLoader.ts src/FileConfigLoader.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ type AuthOptions = {
1111
personalAccessToken?: string;
1212
};
1313

14-
export class ConfigLoader {
14+
export class FileConfigLoader {
1515
static async load(configPath: string | null): Promise<Config> {
16-
const fileConfig = ConfigLoader.readConfigFromFile(configPath);
17-
const authOptions = await ConfigLoader.promptUserAndPassIfNotSet(
18-
ConfigLoader.useAuthOptionsFromEnvIfPresent(ConfigLoader.authOptionsFromFileConfig(fileConfig)),
16+
const fileConfig = FileConfigLoader.readConfigFromFile(configPath);
17+
const authOptions = await FileConfigLoader.promptUserAndPassIfNotSet(
18+
FileConfigLoader.useAuthOptionsFromEnvIfPresent(FileConfigLoader.authOptionsFromFileConfig(fileConfig)),
1919
);
20-
return ConfigLoader.createConfig(fileConfig, ConfigLoader.createAuthorizationToken(authOptions));
20+
return FileConfigLoader.createConfig(fileConfig, FileConfigLoader.createAuthorizationToken(authOptions));
2121
}
2222

2323
private static readConfigFromFile(configPath: string | null, authorizationToken?: string): FileConfig {

src/ObjectConfigLoader.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Config } from "./types/Config";
2+
import { ObjectConfig } from "./types/ObjectConfig";
3+
import path from "path";
4+
5+
type AuthOptions = {
6+
user?: string;
7+
pass?: string;
8+
personalAccessToken?: string;
9+
};
10+
11+
export class ObjectConfigLoader {
12+
static async load(objectConfig: ObjectConfig): Promise<Config> {
13+
const authOptions = await ObjectConfigLoader.fetchCredentialsFromObject(objectConfig);
14+
return ObjectConfigLoader.createConfig(objectConfig, ObjectConfigLoader.createAuthorizationToken(authOptions));
15+
}
16+
17+
private static async fetchCredentialsFromObject(objectConfig: ObjectConfig): Promise<AuthOptions> {
18+
const hasUserPass = !!(objectConfig.user && objectConfig.pass);
19+
const hasPersonalAccessToken = !!objectConfig.personalAccessToken;
20+
if (!hasPersonalAccessToken && !hasUserPass) {
21+
throw new Error("Missing configuration! Config object does not provide a combination of your Confluence username and password or a personal access token.");
22+
}
23+
return {
24+
user: objectConfig.user,
25+
pass: objectConfig.pass,
26+
personalAccessToken: objectConfig.personalAccessToken,
27+
};
28+
}
29+
30+
private static createAuthorizationToken(authOptions: AuthOptions): string {
31+
if (authOptions.personalAccessToken) {
32+
return `Bearer ${authOptions.personalAccessToken}`;
33+
}
34+
35+
if (authOptions.user && authOptions.user.length > 0 && authOptions.pass && authOptions.pass.length > 0) {
36+
const encodedBasicToken = Buffer.from(`${authOptions.user}:${authOptions.pass}`).toString("base64");
37+
return `Basic ${encodedBasicToken}`;
38+
}
39+
40+
throw new Error("Missing configuration! Config object does not provide a combination of your Confluence username and password or a personal access token.");
41+
}
42+
43+
private static normalizeFilePaths(objectConfig: ObjectConfig): ObjectConfig {
44+
for (const i in objectConfig.pages) {
45+
objectConfig.pages[i].file = path.isAbsolute(objectConfig.pages[i].file)
46+
? objectConfig.pages[i].file
47+
: path.resolve(path.dirname(objectConfig.fileRoot) + "/" + objectConfig.pages[i].file);
48+
}
49+
50+
return objectConfig;
51+
}
52+
53+
private static createConfig(objectConfig: ObjectConfig, authorizationToken: string): Config {
54+
const config = ObjectConfigLoader.normalizeFilePaths(objectConfig);
55+
return {
56+
baseUrl: config.baseUrl,
57+
cachePath: config.cachePath,
58+
prefix: config.prefix,
59+
pages: config.pages,
60+
configPath: config.fileRoot || process.cwd(),
61+
customRenderer: config.customRenderer,
62+
authorizationToken: authorizationToken,
63+
}
64+
}
65+
66+
}

src/UpdatePage.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ function convertToWikiFormat(pageData: Page, config: Config) {
8989
if (!pageData.title) {
9090
[pageData.title, fileData] = extractTitle(fileData);
9191
}
92+
const renderer = config.customRenderer
93+
? new config.customRenderer({}, config, pageData)
94+
: new ConfluenceRenderer({}, config, pageData)
9295

9396
return marked(fileData, {
94-
renderer: new ConfluenceRenderer({}, config, pageData),
97+
renderer,
9598
xhtml: true,
9699
});
97100
}

src/cli/MainCommand.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Config } from "../types/Config";
2-
import { ConfigLoader } from "../ConfigLoader";
2+
import { FileConfigLoader } from "../FileConfigLoader";
33
import { ConfluenceAPI } from "../api/ConfluenceAPI";
44
import { updatePage } from "../UpdatePage";
55

66
export default async function(configPath: string | null, force: boolean = false, insecure: boolean = false) {
7-
const config: Config = await ConfigLoader.load(configPath);
7+
const config: Config = await FileConfigLoader.load(configPath);
88
const confluenceAPI = new ConfluenceAPI(config.baseUrl, config.authorizationToken, insecure);
99

1010
for (const pageData of config.pages) {

src/lib/index.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Config } from "../types/Config";
2+
import { ObjectConfigLoader } from "../ObjectConfigLoader";
3+
import { ConfluenceAPI } from "../api/ConfluenceAPI";
4+
import { updatePage } from "../UpdatePage";
5+
import { ObjectConfig } from "../types/ObjectConfig";
6+
7+
const DEFAULTS = {
8+
insecure: false,
9+
force: false,
10+
fileRoot: process.cwd(),
11+
}
12+
13+
export default async function(configOptions: ObjectConfig) {
14+
const config: Config = await ObjectConfigLoader.load(Object.assign({}, DEFAULTS, configOptions));
15+
const confluenceAPI = new ConfluenceAPI(config.baseUrl, config.authorizationToken, configOptions.insecure);
16+
17+
for (const pageData of config.pages) {
18+
await updatePage(confluenceAPI, pageData, config, configOptions.force);
19+
}
20+
}

src/types/BaseConfig.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Page } from "./Page";
2+
3+
export type BaseConfig = {
4+
baseUrl: string;
5+
cachePath: string;
6+
prefix: string;
7+
pages: Page[];
8+
};

src/types/Config.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { Page } from "./Page";
1+
import { BaseConfig } from "./BaseConfig";
2+
import { RendererConstructor } from "./RendererConstructor";
23

3-
export type Config = {
4-
baseUrl: string;
5-
cachePath: string;
6-
prefix: string;
7-
pages: Page[];
4+
export type Config = BaseConfig & {
85
configPath: string;
96
authorizationToken: string;
7+
customRenderer?: RendererConstructor;
108
};

src/types/FileConfig.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import { Page } from "./Page";
1+
import { BaseConfig } from "./BaseConfig";
22

3-
export type FileConfig = {
4-
baseUrl: string;
5-
cachePath: string;
6-
prefix: string;
7-
pages: Page[];
3+
export type FileConfig = BaseConfig & {
84
configPath: string;
95
user?: string;
106
pass?: string;

src/types/ObjectConfig.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { BaseConfig } from "./BaseConfig";
2+
import { RendererConstructor } from "./RendererConstructor";
3+
4+
export type ObjectConfig = BaseConfig & {
5+
fileRoot: string;
6+
user?: string;
7+
pass?: string;
8+
personalAccessToken?: string;
9+
insecure: boolean;
10+
force: boolean;
11+
customRenderer?: RendererConstructor,
12+
};

src/types/RendererConstructor.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Page } from "./Page";
2+
import marked, { Renderer } from "marked";
3+
import { Config } from "./Config";
4+
5+
export interface RendererConstructor {
6+
new (options: marked.MarkedOptions, config: Config, page: Page): Renderer;
7+
}

tests/ConfluenceRenderer.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import ConfluenceRenderer from "../src/ConfluenceRenderer";
2-
import { Page } from "../src/types/Page";
32
import { Config } from "../src/types/Config";
43

54
describe("ConfluenceRenderer", () => {

tests/ConfigLoader.test.ts tests/FileConfigLoader.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ConfigLoader } from "../src/ConfigLoader";
1+
import { FileConfigLoader } from "../src/FileConfigLoader";
22

3-
describe("ConfigLoader", () => {
3+
describe("FileConfigLoader", () => {
44
it("should create bearer token from personal access token", async () => {
55
expect(
6-
await ConfigLoader.load(__dirname + "/resources/test-config-with-personal-access-token-auth.json"),
6+
await FileConfigLoader.load(__dirname + "/resources/test-config-with-personal-access-token-auth.json"),
77
).toEqual({
88
authorizationToken: "Bearer unbearable",
99
configPath: __dirname + "/resources/test-config-with-personal-access-token-auth.json",
@@ -12,7 +12,7 @@ describe("ConfigLoader", () => {
1212
});
1313

1414
it("should create base64 basic token from username and password", async () => {
15-
expect(await ConfigLoader.load(__dirname + "/resources/test-config-with-user-pass-auth.json")).toEqual({
15+
expect(await FileConfigLoader.load(__dirname + "/resources/test-config-with-user-pass-auth.json")).toEqual({
1616
authorizationToken: "Basic dXNlcjpwYXNz",
1717
configPath: __dirname + "/resources/test-config-with-user-pass-auth.json",
1818
...irrelevantConfigFields,

tests/ObjectConfigLoader.test.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ObjectConfigLoader } from "../src/ObjectConfigLoader";
2+
3+
describe("ObjectConfigLoader", () => {
4+
5+
const objectConfigurationWithPersonalAccessToken = {
6+
baseUrl: "https://confluence.custom.host/rest/api",
7+
personalAccessToken: "unbearable",
8+
prefix: "This document is automatically generated. Please don't edit it directly!",
9+
cachePath: "cache/",
10+
fileRoot: __dirname,
11+
pages: [
12+
{
13+
pageId: "123456789",
14+
file: "./tests/README.md"
15+
}
16+
],
17+
insecure: false,
18+
force: false,
19+
}
20+
21+
const objectConfigurationWithUserPass = {
22+
baseUrl: "https://confluence.custom.host/rest/api",
23+
user: "user",
24+
pass: "pass",
25+
prefix: "This document is automatically generated. Please don't edit it directly!",
26+
fileRoot: __dirname,
27+
cachePath: "cache/",
28+
pages: [
29+
{
30+
pageId: "123456789",
31+
file: "./tests/README.md"
32+
}
33+
],
34+
insecure: false,
35+
force: false,
36+
}
37+
38+
it("should create bearer token from personal access token", async () => {
39+
expect(
40+
await ObjectConfigLoader.load(objectConfigurationWithPersonalAccessToken),
41+
).toEqual({
42+
authorizationToken: "Bearer unbearable",
43+
configPath: __dirname,
44+
...irrelevantConfigFields,
45+
});
46+
});
47+
48+
it("should create base64 basic token from username and password", async () => {
49+
expect(await ObjectConfigLoader.load(objectConfigurationWithUserPass)).toEqual({
50+
authorizationToken: "Basic dXNlcjpwYXNz",
51+
configPath: __dirname,
52+
...irrelevantConfigFields,
53+
});
54+
});
55+
56+
const irrelevantConfigFields = {
57+
baseUrl: "https://confluence.custom.host/rest/api",
58+
cachePath: "cache/",
59+
pages: [
60+
{
61+
file: __dirname + "/README.md",
62+
pageId: "123456789",
63+
},
64+
],
65+
prefix: "This document is automatically generated. Please don't edit it directly!",
66+
customRenderer: undefined,
67+
};
68+
});

0 commit comments

Comments
 (0)