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,41 @@
# @univerjs/docs-hyper-link
[![npm version](https://img.shields.io/npm/v/@univerjs/docs-hyper-link?style=flat-square)](https://npmjs.com/package/@univerjs/docs-hyper-link)
[![license](https://img.shields.io/npm/l/@univerjs/docs-hyper-link?style=flat-square)](https://npmjs.com/package/@univerjs/docs-hyper-link)
[![downloads](https://img.shields.io/npm/dm/@univerjs/docs-hyper-link?style=flat-square)](https://npmjs.com/package/@univerjs/docs-hyper-link)
`@univerjs/docs-hyper-link` provides the core hyperlink model and commands for Univer Docs.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/docs-hyper-link` | `UniverDocsHyperLink` | No | No | No |
## Installation
```sh
pnpm add @univerjs/docs-hyper-link
# or
npm install @univerjs/docs-hyper-link
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import { UniverDocsHyperLinkPlugin } from '@univerjs/docs-hyper-link';
univer.registerPlugin(UniverDocsHyperLinkPlugin);
```
## Integration Notes
Use this package with `@univerjs/docs-hyper-link-ui` when users need hyperlink editing UI.
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/docs-hyper-link)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,74 @@
{
"name": "@univerjs/docs-hyper-link",
"version": "1.0.0-rc.0",
"private": false,
"description": "Hyperlink model and commands for Univer Docs.",
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
"license": "Apache-2.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/univer"
},
"homepage": "https://univer.ai",
"repository": {
"type": "git",
"url": "https://github.com/dream-num/univer"
},
"bugs": {
"url": "https://github.com/dream-num/univer/issues"
},
"keywords": [
"univer",
"docs",
"hyperlink",
"link",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"build:bundle": "univer-cli build",
"build:types": "tsc -p tsconfig.node.json",
"build": "pnpm run build:bundle && pnpm run build:types"
},
"dependencies": {
"@univerjs/core": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

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);
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/node",
"compilerOptions": {
"rootDir": "src",
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": false,
"outDir": "lib/types"
},
"include": [
"src"
],
"exclude": [
"src/**/__tests__/**",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();