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
一些检查失败了
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
这个提交包含在:
@@ -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');
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户