snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, expect, it } from 'vitest';
import { DocHyperLinkCommandId } from '../index';
describe('DocHyperLinkCommandId', () => {
it('keeps the public command identifiers stable', () => {
expect(DocHyperLinkCommandId).toEqual({
Add: 'docs.command.add-hyper-link',
Update: 'docs.command.update-hyper-link',
Delete: 'docs.command.delete-hyper-link',
});
});
});

View File

@@ -0,0 +1,42 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
export const AddHyperLinkMuatation: ICommand = {
id: 'docs.mutation.add-hyper-link',
type: CommandType.MUTATION,
handler: () => {
return true;
},
};
export const UpdateHyperLinkMuatation: ICommand = {
id: 'docs.mutation.update-hyper-link',
type: CommandType.MUTATION,
handler: () => {
return true;
},
};
export const DeleteHyperLinkMuatation: ICommand = {
id: 'docs.mutation.delete-hyper-link',
type: CommandType.MUTATION,
handler: () => {
return true;
},
};

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const DOCS_HYPER_LINK_PLUGIN_CONFIG_KEY = 'docs-hyper-link.config';
export const configSymbol = Symbol(DOCS_HYPER_LINK_PLUGIN_CONFIG_KEY);
export interface IUniverDocsHyperLinkConfig {
}
export const defaultPluginConfig: IUniverDocsHyperLinkConfig = {};

View File

@@ -0,0 +1,187 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DocumentDataModel, ICustomRange, IDocumentData } from '@univerjs/core';
import {
CustomRangeType,
ICommandService,
IResourceManagerService,
LocaleType,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
AddHyperLinkMuatation,
DeleteHyperLinkMuatation,
UpdateHyperLinkMuatation,
} from '../../commands/mutations/hyper-link.mutation';
import { DOC_HYPER_LINK_PLUGIN, DocHyperLinkResourceController } from '../resource.controller';
function createDocData(): IDocumentData {
return {
id: 'doc-1',
locale: LocaleType.EN_US,
title: 'Doc',
body: {
dataStream: 'Body\r\n',
customRanges: [
{
startIndex: 0,
endIndex: 4,
rangeId: 'body-link',
rangeType: CustomRangeType.HYPERLINK,
properties: {
url: 'https://body.old',
},
},
{
startIndex: 0,
endIndex: 4,
rangeId: 'ignored-range',
rangeType: CustomRangeType.COMMENT,
},
],
},
headers: {
'header-1': {
headerId: 'header-1',
body: {
dataStream: 'Header\r\n',
customRanges: [{
startIndex: 0,
endIndex: 6,
rangeId: 'header-link',
rangeType: CustomRangeType.HYPERLINK,
properties: {
url: 'https://header.old',
},
}],
},
},
},
footers: {
'footer-1': {
footerId: 'footer-1',
body: {
dataStream: 'Footer\r\n',
customRanges: [{
startIndex: 0,
endIndex: 6,
rangeId: 'footer-link',
rangeType: CustomRangeType.HYPERLINK,
}],
},
},
},
documentStyle: {
pageSize: {
width: 594.3,
height: 840.51,
},
marginTop: 72,
marginBottom: 72,
marginRight: 90,
marginLeft: 90,
},
};
}
function getRange(model: DocumentDataModel, rangeId: string, segment: 'body' | 'header' | 'footer' = 'body'): ICustomRange | undefined {
const doc = segment === 'header'
? model.headerModelMap.get('header-1')
: segment === 'footer'
? model.footerModelMap.get('footer-1')
: model;
return doc?.getBody()?.customRanges?.find((range) => range.rangeId === rangeId);
}
describe('DocHyperLinkResourceController', () => {
let univer: Univer;
let commandService: ICommandService;
let resourceManagerService: IResourceManagerService;
beforeEach(() => {
univer = new Univer();
const injector = univer.__getInjector();
injector.add([DocHyperLinkResourceController]);
injector.get(DocHyperLinkResourceController);
commandService = injector.get(ICommandService);
commandService.registerCommand(AddHyperLinkMuatation);
commandService.registerCommand(UpdateHyperLinkMuatation);
commandService.registerCommand(DeleteHyperLinkMuatation);
resourceManagerService = injector.get(IResourceManagerService);
});
afterEach(() => {
univer.dispose();
});
it('should serialize hyperlink resources from headers, footers and body only', () => {
univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData());
const resource = resourceManagerService.getResourcesByType('doc-1', UniverInstanceType.UNIVER_DOC)
.find((item) => item.name === DOC_HYPER_LINK_PLUGIN);
expect(resource).toBeDefined();
expect(JSON.parse(resource!.data)).toEqual({
links: [
{ id: 'header-link', payload: 'https://header.old' },
{ id: 'footer-link', payload: '' },
{ id: 'body-link', payload: 'https://body.old' },
],
});
});
it('should load hyperlink resources back into header, footer and body ranges', () => {
const doc = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData());
resourceManagerService.loadResources(doc.getUnitId(), [{
name: DOC_HYPER_LINK_PLUGIN,
data: JSON.stringify({
links: [
{ id: 'header-link', payload: 'https://header.new' },
{ id: 'footer-link', payload: 'https://footer.new' },
{ id: 'body-link', payload: 'https://body.new' },
{ id: 'missing-link', payload: 'https://missing.new' },
],
}),
}]);
expect(getRange(doc, 'header-link', 'header')?.properties).toEqual({ url: 'https://header.new' });
expect(getRange(doc, 'footer-link', 'footer')?.properties).toEqual({ url: 'https://footer.new' });
expect(getRange(doc, 'body-link')?.properties).toEqual({ url: 'https://body.new' });
expect(getRange(doc, 'ignored-range')?.properties).toBeUndefined();
});
it('should return an empty resource payload when the document does not exist', () => {
const resource = resourceManagerService.getResourcesByType('missing-doc', UniverInstanceType.UNIVER_DOC)
.find((item) => item.name === DOC_HYPER_LINK_PLUGIN);
expect(resource).toBeDefined();
expect(resource?.data).toBe(JSON.stringify({ links: [] }));
});
it('should execute hyperlink resource mutations successfully', async () => {
await expect(commandService.executeCommand(AddHyperLinkMuatation.id)).resolves.toBe(true);
await expect(commandService.executeCommand(UpdateHyperLinkMuatation.id)).resolves.toBe(true);
await expect(commandService.executeCommand(DeleteHyperLinkMuatation.id)).resolves.toBe(true);
});
});

View File

@@ -0,0 +1,104 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DocumentDataModel, ICustomRange } from '@univerjs/core';
import { CustomRangeType, Disposable, Inject, IResourceManagerService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
export const DOC_HYPER_LINK_PLUGIN = 'DOC_HYPER_LINK_PLUGIN';
interface IDocHyperLinkJSON {
links: { id: string; payload: string }[];
}
export class DocHyperLinkResourceController extends Disposable {
constructor(
@Inject(IResourceManagerService) private readonly _resourceManagerService: IResourceManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._init();
}
private _init() {
this._resourceManagerService.registerPluginResource({
pluginName: DOC_HYPER_LINK_PLUGIN,
businesses: [UniverInstanceType.UNIVER_DOC],
onLoad: (unitID: string, resource: IDocHyperLinkJSON) => {
const doc = this._univerInstanceService.getUnit<DocumentDataModel>(unitID, UniverInstanceType.UNIVER_DOC);
if (!doc) {
return;
}
const customRangeMap = new Map<string, ICustomRange>();
const handleDoc = (model: DocumentDataModel) => {
model.getBody()?.customRanges?.forEach((customRange) => {
if (customRange.rangeType === CustomRangeType.HYPERLINK) {
customRangeMap.set(customRange.rangeId, customRange);
}
});
return customRangeMap;
};
doc.headerModelMap.forEach((headerModel) => {
handleDoc(headerModel);
});
doc.footerModelMap.forEach((footerModel) => {
handleDoc(footerModel);
});
handleDoc(doc);
resource.links.forEach((link) => {
const customRange = customRangeMap.get(link.id);
if (customRange) {
customRange.properties = {
...customRange.properties,
url: link.payload,
};
}
});
},
onUnLoad: (unitID: string) => {},
toJson: (unitID: string) => {
const doc = this._univerInstanceService.getUnit<DocumentDataModel>(unitID, UniverInstanceType.UNIVER_DOC);
const links: { id: string; payload: string }[] = [];
if (doc) {
const handleDoc = (model: DocumentDataModel) => {
model.getBody()?.customRanges?.forEach((customRange) => {
if (customRange.rangeType === CustomRangeType.HYPERLINK) {
links.push({
id: customRange.rangeId,
payload: customRange.properties?.url || '',
});
}
});
};
doc.headerModelMap.forEach((headerModel) => {
handleDoc(headerModel);
});
doc.footerModelMap.forEach((footerModel) => {
handleDoc(footerModel);
});
handleDoc(doc);
}
return JSON.stringify({ links });
},
parseJson(bytes: string): IDocHyperLinkJSON {
return JSON.parse(bytes);
},
});
}
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum DocHyperLinkCommandId {
Add = 'docs.command.add-hyper-link',
Update = 'docs.command.update-hyper-link',
Delete = 'docs.command.delete-hyper-link',
}
export type { IUniverDocsHyperLinkConfig } from './config/config';
export { UniverDocsHyperLinkPlugin } from './plugin';

View File

@@ -0,0 +1,58 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import type { IUniverDocsHyperLinkConfig } from './config/config';
import { ICommandService, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
import pkg from '../package.json';
import { AddHyperLinkMuatation, DeleteHyperLinkMuatation, UpdateHyperLinkMuatation } from './commands/mutations/hyper-link.mutation';
import { defaultPluginConfig, DOCS_HYPER_LINK_PLUGIN_CONFIG_KEY } from './config/config';
import { DOC_HYPER_LINK_PLUGIN, DocHyperLinkResourceController } from './controllers/resource.controller';
export class UniverDocsHyperLinkPlugin extends Plugin {
static override pluginName = DOC_HYPER_LINK_PLUGIN;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_DOC;
constructor(
private readonly _config: Partial<IUniverDocsHyperLinkConfig> = defaultPluginConfig,
@Inject(Injector) protected override _injector: Injector,
@IConfigService private readonly _configService: IConfigService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(DOCS_HYPER_LINK_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
const deps: Dependency[] = [[DocHyperLinkResourceController]];
deps.forEach((dep) => this._injector.add(dep));
[AddHyperLinkMuatation, DeleteHyperLinkMuatation, UpdateHyperLinkMuatation].forEach((mutation) => {
this.disposeWithMe(this._commandService.registerCommand(mutation));
});
this._injector.get(DocHyperLinkResourceController);
}
}