feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,28 @@
import cssToTheme from './cssToTheme';
describe('cssToTheme', () => {
it('should convert a CSS string to a theme', async () => {
const input = `
:root {
--joplin-appearance: light;
--joplin-color: #333333;
--joplin-background-color: #778899;
/* Should skip this comment and empty lines */
--joplin-background-color-transparent: rgba(255,255,255,0.9);
}`;
const expected = {
appearance: 'light',
color: '#333333',
backgroundColor: '#778899',
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
};
const actual = cssToTheme(input, 'test.css');
expect(actual).toEqual(expected);
});
});

查看文件

@@ -0,0 +1,53 @@
import { Theme } from '../../themes/type';
// Need to include it that way due to a bug in the lib:
// https://github.com/reworkcss/css/pull/146#issuecomment-740412799
import { CssRuleAST, CssTypes, parse as cssParse } from '@adobe/css-tools';
function formatCssToThemeVariable(cssVariable: string): string {
const elements = cssVariable.substr(2).split('-');
if (elements[0] !== 'joplin') throw new Error(`CSS variable name must start with "--joplin": ${cssVariable}`);
elements.splice(0, 1);
return elements.map((e, i) => {
const c = i === 0 ? e[0] : e[0].toUpperCase();
return c + e.substr(1);
}).join('');
}
// function unquoteValue(v:string):string {
// if (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"')) return v.substr(1, v.length - 2);
// return v;
// }
export default function cssToTheme(css: string, sourceFilePath: string): Theme {
const o = cssParse(css, {
silent: false,
source: sourceFilePath,
});
const rules = o?.stylesheet?.rules;
if (!rules?.length) throw new Error(`Invalid CSS color file: ${sourceFilePath}`);
let rootRule: CssRuleAST|null = null;
for (const rule of rules) {
if (rule.type === CssTypes.rule) {
rootRule = rule;
break;
}
}
if (!rootRule || !rootRule.selectors.includes(':root')) throw new Error('`:root` rule not found');
const declarations = rootRule.declarations;
const output: Record<string, string> = {};
for (const declaration of declarations) {
if (declaration.type !== 'declaration') continue; // Skip comment lines
output[formatCssToThemeVariable(declaration.property)] = declaration.value;
}
return output as unknown as Theme;
}

查看文件

@@ -0,0 +1,27 @@
import { Theme } from '../../themes/type';
import { filename } from '../../path-utils';
import shim from '../../shim';
import cssToTheme from './cssToTheme';
export default async function(cssBaseDir: string): Promise<Record<string, Theme>> {
const themeDirs = (await shim.fsDriver().readDirStats(cssBaseDir)).filter(f => f.isDirectory());
const output: Record<string, Theme> = {};
for (const themeDir of themeDirs) {
const themeName = filename(themeDir.path);
const cssFile = `${cssBaseDir}/${themeDir.path}/colors.css`;
const cssContent = await shim.fsDriver().readFile(cssFile, 'utf8');
let themeId = themeName;
const manifestFile = `${cssBaseDir}/${themeDir.path}/manifest.json`;
if (await shim.fsDriver().exists(manifestFile)) {
const manifest = JSON.parse(await shim.fsDriver().readFile(manifestFile, 'utf8'));
if (manifest.id) themeId = manifest.id;
}
output[themeId] = cssToTheme(cssContent, cssFile);
}
return output;
}

查看文件

@@ -0,0 +1,124 @@
import { Theme, ThemeAppearance } from '../../themes/type';
import themeToCss from './themeToCss';
const input: Theme = {
appearance: ThemeAppearance.Light,
// Color scheme "1" is the basic one, like used to display the note
// content. It's basically dark gray text on white background
backgroundColor: '#ffffff',
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorErrorSelected: '#d00000',
colorCorrect: 'green',
colorWarn: 'rgb(228,86,0)',
colorWarnUrl: '#155BDA',
colorFaded: '#7C8B9E', // For less important text
dividerColor: '#dddddd',
selectedColor: '#e5e5e5',
urlColor: '#155BDA',
shadowColor: 'rgba(0,0,0,0.15)',
// Color scheme "2" is used for the sidebar. It's white text over
// dark blue background.
backgroundColor2: '#313640',
color2: '#ffffff',
selectedColor2: '#131313',
colorError2: '#ff6c6c',
colorWarn2: '#ffcb81',
colorWarn3: '#ff7626',
backgroundColorTransparent2: 'rgba(0, 0, 0, 0.1)',
// Color scheme "3" is used for the config screens for example/
// It's dark text over gray background.
backgroundColor3: '#F4F5F6',
backgroundColorHover3: '#CBDAF1',
color3: '#738598',
// Color scheme "4" is used for secondary-style buttons. It makes a white
// button with blue text.
backgroundColor4: '#ffffff',
backgroundColor4Dimmed: '#e3eaff',
color4: '#2D6BDC',
raisedBackgroundColor: '#e5e5e5',
raisedColor: '#222222',
searchMarkerBackgroundColor: '#F7D26E',
searchMarkerColor: 'black',
warningBackgroundColor: '#FFD08D',
destructiveColor: '#F00000',
tableBackgroundColor: 'rgb(247, 247, 247)',
codeBackgroundColor: 'rgb(243, 243, 243)',
codeBorderColor: 'rgb(220, 220, 220)',
codeColor: 'rgb(0,0,0)',
blockQuoteOpacity: 0.7,
codeMirrorTheme: 'default',
codeThemeCss: 'atom-one-light.css',
headerBackgroundColor: '#ffffff',
textSelectionColor: '#a0a0ff',
colorBright2: '#ffffff',
};
const expected = `
:root {
--joplin-appearance: light;
--joplin-background-color: #ffffff;
--joplin-background-color2: #313640;
--joplin-background-color3: #F4F5F6;
--joplin-background-color4: #ffffff;
--joplin-background-color4-dimmed: #e3eaff;
--joplin-background-color-hover3: #CBDAF1;
--joplin-background-color-transparent: rgba(255,255,255,0.9);
--joplin-background-color-transparent2: rgba(0, 0, 0, 0.1);
--joplin-block-quote-opacity: 0.7;
--joplin-code-background-color: rgb(243, 243, 243);
--joplin-code-border-color: rgb(220, 220, 220);
--joplin-code-color: rgb(0,0,0);
--joplin-code-mirror-theme: default;
--joplin-code-theme-css: atom-one-light.css;
--joplin-color: #32373F;
--joplin-color2: #ffffff;
--joplin-color3: #738598;
--joplin-color4: #2D6BDC;
--joplin-color-bright2: #ffffff;
--joplin-color-correct: green;
--joplin-color-error: red;
--joplin-color-error2: #ff6c6c;
--joplin-color-error-selected: #d00000;
--joplin-color-faded: #7C8B9E;
--joplin-color-warn: rgb(228,86,0);
--joplin-color-warn2: #ffcb81;
--joplin-color-warn3: #ff7626;
--joplin-color-warn-url: #155BDA;
--joplin-destructive-color: #F00000;
--joplin-divider-color: #dddddd;
--joplin-header-background-color: #ffffff;
--joplin-odd-background-color: #eeeeee;
--joplin-raised-background-color: #e5e5e5;
--joplin-raised-color: #222222;
--joplin-search-marker-background-color: #F7D26E;
--joplin-search-marker-color: black;
--joplin-selected-color: #e5e5e5;
--joplin-selected-color2: #131313;
--joplin-shadow-color: rgba(0,0,0,0.15);
--joplin-table-background-color: rgb(247, 247, 247);
--joplin-text-selection-color: #a0a0ff;
--joplin-url-color: #155BDA;
--joplin-warning-background-color: #FFD08D;
}`;
describe('themeToCss', () => {
it('should a theme to a CSS string', async () => {
const actual = themeToCss(input);
expect(actual.trim()).toBe(expected.trim());
});
});

查看文件

@@ -0,0 +1,29 @@
import { Theme } from '../../themes/type';
import { camelCaseToDash, formatCssSize } from '../../string-utils';
const isColor = (v: unknown): v is { color: unknown; model: unknown; valpha: unknown } => {
return !!v && typeof v === 'object' && ('color' in v) && ('model' in v) && ('valpha' in v);
};
export default function(theme: Theme) {
const lines = [];
lines.push(':root {');
const names = Object.keys(theme).sort();
for (const name of names) {
const value = (theme as unknown as Record<string, unknown>)[name];
if (typeof value === 'object' && !isColor(value)) continue;
if (value === undefined || value === null) continue;
if (typeof value === 'number' && isNaN(value)) continue;
const newName = `--joplin-${camelCaseToDash(name)}`;
const formattedValue = typeof value === 'number' && newName.indexOf('opacity') < 0 ? formatCssSize(value) : value;
lines.push(`\t${newName}: ${formattedValue};`);
}
lines.push('}');
return lines.join('\n');
}