Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions __tests__/desktop-stub-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { describe, test, expect } from 'bun:test';
import {
parseSpriteScale,
detectLanguageCode,
extractThemeQueryValue,
findThemeChoice,
normalizeThemeChoice,
resolveUiThemeId,
buildThemeConfig,
shouldUseEastAsiaVariant,
getSpriteOptions,
collectFontNames,
Expand Down Expand Up @@ -72,6 +77,96 @@ describe('detectLanguageCode', () => {
});
});

describe('extractThemeQueryValue', () => {
test('returns empty string for empty or invalid input', () => {
expect(extractThemeQueryValue(undefined)).toBe('');
expect(extractThemeQueryValue(null)).toBe('');
expect(extractThemeQueryValue('')).toBe('');
});

test('extracts theme from query string', () => {
expect(extractThemeQueryValue('?theme=dark')).toBe('dark');
expect(extractThemeQueryValue('?foo=bar&theme=theme-night')).toBe('theme-night');
});
});

describe('findThemeChoice', () => {
test('returns first matching theme from location chain', () => {
expect(findThemeChoice([
'',
'?foo=bar',
'?theme=dark',
'?theme=light'
])).toBe('dark');
});

test('returns empty string when no theme exists', () => {
expect(findThemeChoice(['', '?foo=bar'])).toBe('');
expect(findThemeChoice(null)).toBe('');
});
});

describe('normalizeThemeChoice', () => {
test('defaults to light for empty or unknown values', () => {
expect(normalizeThemeChoice(undefined)).toBe('light');
expect(normalizeThemeChoice(null)).toBe('light');
expect(normalizeThemeChoice('')).toBe('light');
expect(normalizeThemeChoice('system')).toBe('light');
expect(normalizeThemeChoice('theme-system')).toBe('light');
expect(normalizeThemeChoice('unknown')).toBe('light');
});

test('accepts light aliases', () => {
expect(normalizeThemeChoice('light')).toBe('light');
expect(normalizeThemeChoice('theme-classic-light')).toBe('light');
expect(normalizeThemeChoice('theme-white')).toBe('light');
});

test('accepts dark aliases', () => {
expect(normalizeThemeChoice('dark')).toBe('dark');
expect(normalizeThemeChoice('theme-night')).toBe('dark');
expect(normalizeThemeChoice('theme-dark')).toBe('dark');
});
});

describe('resolveUiThemeId', () => {
test('returns explicit ONLYOFFICE theme ids', () => {
expect(resolveUiThemeId('dark')).toBe('theme-night');
expect(resolveUiThemeId('theme-night')).toBe('theme-night');
expect(resolveUiThemeId('light')).toBe('theme-classic-light');
expect(resolveUiThemeId('theme-system')).toBe('theme-classic-light');
expect(resolveUiThemeId(undefined)).toBe('theme-classic-light');
});
});

describe('buildThemeConfig', () => {
test('returns explicit light theme config by default', () => {
expect(buildThemeConfig(undefined)).toEqual({
id: 'theme-classic-light',
type: 'light',
system: 'light'
});
expect(buildThemeConfig('theme-system')).toEqual({
id: 'theme-classic-light',
type: 'light',
system: 'light'
});
});

test('returns explicit dark theme config for dark values', () => {
expect(buildThemeConfig('dark')).toEqual({
id: 'theme-night',
type: 'dark',
system: 'dark'
});
expect(buildThemeConfig('theme-night')).toEqual({
id: 'theme-night',
type: 'dark',
system: 'dark'
});
});
});

describe('shouldUseEastAsiaVariant', () => {
test('returns false for empty/null lang', () => {
expect(shouldUseEastAsiaVariant('')).toBe(false);
Expand Down
94 changes: 94 additions & 0 deletions editors/desktop-stub-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,95 @@ function detectLanguageCode(location) {
return '';
}

/**
* Extract theme query parameter from a search string.
* @param {string} search - URL search string
* @returns {string} Theme query value or empty string
*/
function extractThemeQueryValue(search) {
try {
var searchValue = typeof search === 'string' ? search : '';
if (!searchValue) {
return '';
}
return new URLSearchParams(searchValue).get('theme') || '';
} catch (err) {
return '';
}
}

/**
* Find the first theme value from a list of search strings.
* @param {Array<string>} searches - Ordered search strings to inspect
* @returns {string} First non-empty theme query value or empty string
*/
function findThemeChoice(searches) {
if (!Array.isArray(searches)) {
return '';
}

for (var i = 0; i < searches.length; i++) {
var theme = extractThemeQueryValue(searches[i]);
if (theme) {
return theme;
}
}

return '';
}

/**
* Normalize incoming theme values to the two supported modes.
* Accepts either host-friendly names ("dark"/"light") or ONLYOFFICE theme ids.
* Any unknown value falls back to light.
* @param {string} theme - Theme query parameter or theme id
* @returns {"dark"|"light"} Normalized theme choice
*/
function normalizeThemeChoice(theme) {
var normalized = typeof theme === 'string' ? theme.toLowerCase() : '';
if (
normalized === 'dark' ||
normalized === 'theme-night' ||
normalized === 'theme-dark'
) {
return 'dark';
}
return 'light';
}

/**
* Resolve the ONLYOFFICE uiTheme id for an incoming theme value.
* @param {string} theme - Theme query parameter or theme id
* @returns {string} ONLYOFFICE theme id
*/
function resolveUiThemeId(theme) {
return normalizeThemeChoice(theme) === 'dark'
? 'theme-night'
: 'theme-classic-light';
}

/**
* Build the desktop stub theme object expected by the web apps.
* @param {string} theme - Theme query parameter or theme id
* @returns {{id: string, type: string, system: string}} Theme config
*/
function buildThemeConfig(theme) {
var normalized = normalizeThemeChoice(theme);
if (normalized === 'dark') {
return {
id: 'theme-night',
type: 'dark',
system: 'dark'
};
}

return {
id: 'theme-classic-light',
type: 'light',
system: 'light'
};
}

/**
* Check if East Asian font variant should be used
* @param {string} lang - Language code
Expand Down Expand Up @@ -168,6 +257,11 @@ function extractBlobUrl(path) {
FONT_SPRITE_ROW_HEIGHT: FONT_SPRITE_ROW_HEIGHT,
parseSpriteScale: parseSpriteScale,
detectLanguageCode: detectLanguageCode,
extractThemeQueryValue: extractThemeQueryValue,
findThemeChoice: findThemeChoice,
normalizeThemeChoice: normalizeThemeChoice,
resolveUiThemeId: resolveUiThemeId,
buildThemeConfig: buildThemeConfig,
shouldUseEastAsiaVariant: shouldUseEastAsiaVariant,
getSpriteOptions: getSpriteOptions,
collectFontNames: collectFontNames,
Expand Down
77 changes: 67 additions & 10 deletions editors/desktop-stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,43 @@
var extractMediaFilename = utils.extractMediaFilename || function(p) { return p; };
var buildMediaUrl = utils.buildMediaUrl || function(base, hash, name) { return hash ? base + '/api/media/' + hash + '/' + name : null; };
var extractBlobUrl = utils.extractBlobUrl || function(p) { return null; };
var findThemeChoice = utils.findThemeChoice || function(searches) {
if (!Array.isArray(searches)) {
return '';
}

for (var i = 0; i < searches.length; i++) {
var searchValue = typeof searches[i] === 'string' ? searches[i] : '';
if (!searchValue) {
continue;
}

try {
var theme = new URLSearchParams(searchValue).get('theme');
if (theme) {
return theme;
}
} catch (err) {}
}

return '';
};
var buildThemeConfig = utils.buildThemeConfig || function(theme) {
var normalized = typeof theme === 'string' ? theme.toLowerCase() : '';
if (normalized === 'dark' || normalized === 'theme-night' || normalized === 'theme-dark') {
return {
id: 'theme-night',
type: 'dark',
system: 'dark'
};
}

return {
id: 'theme-classic-light',
type: 'light',
system: 'light'
};
};

function detectLanguageCode() {
if (utils.detectLanguageCode) {
Expand All @@ -117,6 +154,34 @@
return '';
}

function collectLocationSearches() {
var searches = [];
var currentWindow = window;
var visitedWindows = [];

while (currentWindow && visitedWindows.indexOf(currentWindow) === -1) {
visitedWindows.push(currentWindow);

try {
searches.push(currentWindow.location && currentWindow.location.search ? currentWindow.location.search : '');
if (!currentWindow.parent || currentWindow.parent === currentWindow) {
break;
}
currentWindow = currentWindow.parent;
} catch (err) {
break;
}
}

return searches;
}

function detectThemeConfig() {
return buildThemeConfig(findThemeChoice(collectLocationSearches()));
}

var initialThemeConfig = detectThemeConfig();

function shouldUseEastAsiaVariant() {
var lang = detectLanguageCode();
if (!lang && window.Common && window.Common.Locale && typeof window.Common.Locale.getCurrentLanguage === 'function') {
Expand Down Expand Up @@ -264,11 +329,7 @@
},

// Theme support
theme: {
id: 'theme-classic-light',
type: 'light',
system: 'light'
},
theme: { ...initialThemeConfig },

// Crypto plugin support (stub)
CryptoMode: 0,
Expand Down Expand Up @@ -1657,11 +1718,7 @@

// Create RendererProcessVariable stub for theme and RTL support
window.RendererProcessVariable = {
theme: {
id: 'theme-classic-light',
type: 'light',
system: 'light'
},
theme: { ...initialThemeConfig },
localthemes: {},
rtl: false
};
Expand Down
16 changes: 15 additions & 1 deletion editors/offline-loader-proper.html
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,22 @@
return docparams;
}

function resolveUiTheme(urlParams) {
if (window.DesktopStubUtils && typeof window.DesktopStubUtils.resolveUiThemeId === 'function') {
return window.DesktopStubUtils.resolveUiThemeId(urlParams['theme']);
}

var themeParam = typeof urlParams['theme'] === 'string'
? urlParams['theme'].toLowerCase()
: '';
return (themeParam === 'dark' || themeParam === 'theme-night' || themeParam === 'theme-dark')
? 'theme-night'
: 'theme-classic-light';
}

function getEditorConfig(urlParams) {
var headerLogoUrl = window.location.origin + '/web-apps/apps/common/main/resources/img/header/header-logo_s.svg';
var uiTheme = resolveUiTheme(urlParams);
return {
customization : {
goback: { url: "onlyoffice.com" },
Expand All @@ -423,7 +437,7 @@
hideRightMenu: false,
showReviewChanges: false,
toolbarNoTabs: false,
uiTheme: 'theme-classic-light', // Prevent theme switching dialogs
uiTheme: uiTheme,
autosave: false, // Disable autosave completely
logo: {
visible: true,
Expand Down
4 changes: 4 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ app.get('/open', (req, res) => {
const filepath = req.query.filepath;
// NOTE(victor): OnlyOffice expects ISO 639-1 codes (e.g. "es", "fr"), not full names like "spanish"
const lang = typeof req.query.lang === 'string' ? req.query.lang : '';
const theme = typeof req.query.theme === 'string' ? req.query.theme : '';

if (!filepath) {
return res.status(400).json({ error: 'filepath query parameter is required' });
Expand Down Expand Up @@ -896,6 +897,9 @@ app.get('/open', (req, res) => {
if (lang) {
redirectParams.set('lang', lang);
}
if (theme) {
redirectParams.set('theme', theme);
}

res.redirect(`/offline-loader-proper.html?${redirectParams.toString()}`);
});
Expand Down
Loading