Compare commits

...

36 Commits

Author SHA1 Message Date
55f811af62 Merge remote-tracking branch 'origin/release/v9.4.0' into develop 2026-03-27 09:16:10 +03:00
057f0bbbf6 Add scripts for thumbnail for site 2026-03-26 16:10:46 +03:00
67060cc66e Merge pull request 'master' (#177) from master into release/v9.4.0 2026-03-24 08:47:58 +00:00
c5f6c2e02b Merge pull request 'fix/js-doc' (#176) from fix/js-doc into master 2026-03-24 08:45:33 +00:00
71b912e7e6 [jsdoc] Added missed calls get_translation method 2026-03-24 15:31:23 +07:00
6e7fd50583 [jsdoc] Refactor resolve @link tag 2026-03-24 13:39:38 +07:00
cf39498098 [jsdoc] Don't use default path for translations 2026-03-24 13:37:32 +07:00
1f4e593943 fix readme 2026-03-19 12:55:28 +03:00
3dfd22c735 [jsdoc] Fix lang for reserved link 2026-03-19 15:02:25 +07:00
f92fc0f617 Merge branch hotfix/v9.3.1 into release/v9.4.0 2026-03-12 15:16:28 +00:00
0402a5a07a Merge branch hotfix/v9.3.1 into develop 2026-03-12 15:16:27 +00:00
dea91ca6f6 Merge branch hotfix/v9.3.1 into master 2026-03-12 15:16:26 +00:00
4be0e0cbe2 [jsdoc] Fix write missed/unused translations 2026-03-11 16:32:11 +00:00
1e8825e15e [develop] Improve README with fixes and Git Bash note 2026-03-11 19:30:43 +03:00
9d17f87811 fix/js-doc (#174)
Add translations
Co-authored-by: Nikita Khromov <Nikita.Khromov@onlyoffice.com>
2026-03-11 09:24:59 +00:00
2fff3a7391 [develop] Skip empty addon dirs and log npm steps in build_server_with_addons 2026-03-11 01:35:21 +03:00
c0bdb1d62b [socketio] Up module version 2026-03-09 21:15:54 +03:00
7c97a9b326 Merge branch hotfix/v9.3.1 into master 2026-03-05 07:44:16 +00:00
b33d92a32e Merge pull request 'Rework Dockerfile for Ubuntu 24.04 base image' (#173) from fix/dockerfile into hotfix/v9.3.1
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/173
2026-03-04 13:43:26 +00:00
d480266a5a Rework Dockerfile for Ubuntu 24.04 base image 2026-03-04 18:37:55 +05:00
98af1ed74b Merge pull request 'release/v9.3.0' (#172) from release/v9.3.0 into master 2026-03-03 17:53:56 +00:00
9428ce8b33 Merge branch hotfix/v9.3.1 into master 2026-03-03 11:55:43 +00:00
c1dbdc39f1 Merge pull request 'Up version 9.3.1' (#171) from fix/version9.3.1 into hotfix/v9.3.1
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/171
2026-03-03 09:45:04 +00:00
5d99680cc4 Up version 9.3.1 2026-03-03 14:36:02 +05:00
930b11f19e Merge pull request '[jsdoc] Fix prev' (#170) from fix/js-doc into release/v9.3.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/170
2026-02-27 10:33:00 +00:00
62aa75d82c [jsdoc] Fix prev 2026-02-27 17:32:02 +07:00
f926970677 Merge pull request '[jsdoc] Fix generate paths for enumerations' (#169) from fix/js-doc into release/v9.3.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/169
2026-02-26 11:35:50 +00:00
4f6908154c [jsdoc] Fix generate paths for enumerations 2026-02-26 18:34:03 +07:00
151c4e7d2f Merge branch release/v9.3.0 into develop 2026-02-25 15:13:49 +00:00
923d839483 [build] Reapply server-admin-panel addon check before building admin panel 2026-02-24 11:38:28 +03:00
39b1c1e22c [server] Build adminpanel from server-admin-panel private repository 2026-02-24 11:38:28 +03:00
39fb488af8 Merge remote-tracking branch 'origin/release/v9.3.0' into develop 2026-02-17 22:39:07 +03:00
42b57d6b40 Merge branch 'release/v9.3.0' into develop 2026-02-03 10:57:03 +03:00
4dda7dfa7a Merge branch 'release/v9.3.0' into develop 2026-01-14 15:40:31 +03:00
7cbaa00356 Merge branch hotfix/v9.2.1 into develop 2025-12-26 16:09:43 +00:00
4d812fa6d2 Merge branch hotfix/v9.2.1 into develop 2025-12-17 15:25:35 +00:00
11 changed files with 801 additions and 234 deletions

View File

@ -1,4 +1,4 @@
FROM ubuntu:20.04
FROM ubuntu:24.04
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
@ -9,25 +9,37 @@ RUN echo 'keyboard-configuration keyboard-configuration/layoutcode string us' |
echo 'keyboard-configuration keyboard-configuration/modelcode string pc105' | debconf-set-selections
RUN apt-get -y update && \
apt-get -y install tar \
sudo \
wget
RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.0/cmake-3.30.0-linux-x86_64.tar.gz && \
tar -xzf cmake-3.30.0-linux-x86_64.tar.gz -C /opt && \
ln -s /opt/cmake-3.30.0-linux-x86_64/bin/cmake /usr/local/bin/cmake && \
ln -s /opt/cmake-3.30.0-linux-x86_64/bin/ctest /usr/local/bin/ctest && \
rm cmake-3.30.0-linux-x86_64.tar.gz
apt-get -y install sudo \
git \
git-lfs \
curl \
wget \
p7zip-full
ADD . /build_tools
WORKDIR /build_tools
RUN mkdir -p /opt/python3 && \
wget -P /opt/python3/ https://github.com/ONLYOFFICE-data/build_tools_data/raw/refs/heads/master/python/python3.tar.gz && \
tar -xzf /opt/python3/python3.tar.gz -C /opt/python3 --strip-components=1
# Install local Python
RUN cd tools/linux && \
./python.sh
ENV PATH="/opt/python3/bin:${PATH}"
# Fetch Qt binaries
RUN cd tools/linux && \
./python3/bin/python3 ./qt_binary_fetch.py amd64
RUN ln -s /opt/python3/bin/python3.10 /usr/bin/python
# Install system dependencies
RUN cd tools/linux && \
./python3/bin/python3 ./deps.py
CMD ["sh", "-c", "cd tools/linux && python3 ./automate.py"]
# Install CMake
RUN cd tools/linux && \
./cmake.sh
# Fetch sysroot
RUN cd tools/linux/sysroot && \
../python3/bin/python3 ./fetch.py amd64
ARG BRANCH=master
ENV BRANCH=${BRANCH}
CMD ["sh", "-c", "./tools/linux/python3/bin/python3 ./configure.py --sysroot \"1\" --clean \"0\" --update-light \"1\" --branch \"${BRANCH}\" --update \"1\" --module \"desktop server builder\" --qt-dir \"$(pwd)/tools/linux/qt_build/Qt-5.9.9\" && ./tools/linux/python3/bin/python3 ./make.py"]

View File

@ -1,15 +1,11 @@
# Docker
This directory containing instruction for developers,
who want to change something in sdkjs or web-apps or server module,
but don't want to compile pretty compilcated core product to make those changes.
This directory contains instructions for developers,
who want to change something in sdkjs, web-apps, or the server module,
but don't want to compile the complicated core product to make those changes.
## System requirements
**Note**: ARM-based architectures are currently **NOT** supported;
attempting to run the images on ARM devices may result in startup failures
or other runtime issues.
### Windows
You need the latest
@ -29,22 +25,22 @@ You need the latest
[Docker](https://docs.docker.com/engine/install/)
version installed.
## Create develop Docker Images
## Create Development Docker Image
To create a image with the ability to include external non-minified sdkjs code,
To create an image with the ability to include external non-minified sdkjs code,
use the following commands:
### Clone development environment to work dir
### Clone development environment to the working directory
```bash
git clone https://github.com/ONLYOFFICE/build_tools.git
```
### Modify Docker Images
### Build Docker Image
**Note**: Do not prefix docker command with sudo.
[This](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
instruction show how to use docker without sudo.
**Note**: Do not prefix the docker command with sudo.
[These instructions](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
show how to use docker without sudo.
```bash
cd build_tools/develop
@ -54,11 +50,11 @@ docker build --no-cache -t documentserver-develop .
**Note**: The dot at the end is required.
**Note**: Sometimes script may fail due to network errors. Just restart it.
**Note**: Sometimes the build may fail due to network errors. Just restart it.
## Clone development modules
Clone development modules to the work dir
Clone development modules to the working directory.
* `sdkjs` repo is located [here](https://github.com/ONLYOFFICE/sdkjs/)
* `web-apps` repo is located [here](https://github.com/ONLYOFFICE/web-apps/)
@ -76,43 +72,49 @@ To mount external folders to the container,
you need to pass the "-v" parameter
along with the relative paths to the required folders.
The folders `sdkjs` and `web-apps` are required for proper development workflow.
The folders `server` is optional
The folder `server` is optional.
**Note**: Run command with the current working directory
**Note**: Run the command with the current working directory
containing `sdkjs`, `web-apps`...
**Note**: ONLYOFFICE server uses port 80.
Look for another application using port 80 and stop it
Look for another application using port 80 and stop it.
**Note**: Server start with `sdkjs` and `web-apps` takes 15 minutes
and takes 20 minutes with `server`
**Note**: Starting the server with `sdkjs` and `web-apps` takes 15 minutes,
or 20 minutes with `server`.
### docker run on Windows (PowerShell)
**Note**: Run PowerShell as administrator to fix EACCES error when installing
node_modules
node_modules.
run with `sdkjs` and `web-apps`
Run with `sdkjs` and `web-apps`
```bash
```powershell
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v $pwd/sdkjs:/var/www/onlyoffice/documentserver/sdkjs -v $pwd/web-apps:/var/www/onlyoffice/documentserver/web-apps documentserver-develop
```
or run with `sdkjs`, `web-apps` and `server`
Or run with `sdkjs`, `web-apps`, and `server`
```powershell
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v $pwd/sdkjs:/var/www/onlyoffice/documentserver/sdkjs -v $pwd/web-apps:/var/www/onlyoffice/documentserver/web-apps -v $pwd/server:/var/www/onlyoffice/documentserver/server documentserver-develop
```
**Note**: If using Git Bash instead of PowerShell, you may need to quote the paths:
```bash
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v $pwd/sdkjs:/var/www/onlyoffice/documentserver/sdkjs -v $pwd/web-apps:/var/www/onlyoffice/documentserver/web-apps -v $pwd/server:/var/www/onlyoffice/documentserver/server documentserver-develop
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v "$(pwd)/sdkjs":/var/www/onlyoffice/documentserver/sdkjs -v "$(pwd)/web-apps":/var/www/onlyoffice/documentserver/web-apps documentserver-develop
```
### docker run on Linux or macOS
run with `sdkjs` and `web-apps`
Run with `sdkjs` and `web-apps`
```bash
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v $(pwd)/sdkjs:/var/www/onlyoffice/documentserver/sdkjs -v $(pwd)/web-apps:/var/www/onlyoffice/documentserver/web-apps documentserver-develop
```
or run with `sdkjs`, `web-apps` and `server`
Or run with `sdkjs`, `web-apps`, and `server`
```bash
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v $(pwd)/sdkjs:/var/www/onlyoffice/documentserver/sdkjs -v $(pwd)/web-apps:/var/www/onlyoffice/documentserver/web-apps -v $(pwd)/server:/var/www/onlyoffice/documentserver/server documentserver-develop
@ -120,93 +122,99 @@ docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true -v $
## Open editor
After the server starts successfully, you will see Docker log messages like this
After the server starts successfully, you will see Docker log messages like this.
```bash
```text
[Date] [WARN] [localhost] [docId] [userId] nodeJS
```
To try the document editor, open a browser tab and type
[http://localhost/example](http://localhost/example) into the URL bar.
**Note**: Disable **ad blockers** for localhost page.
It may block some scripts (like Analytics.js)
**Note**: Disable **ad blockers** for the localhost page.
They may block some scripts (like Analytics.js).
## Modify sources
### To change something in `sdkjs` do the following steps
### To change something in `sdkjs`, do the following steps
1)Edit source file. Let's insert an image url into each open document.
Following command inserts (in case of problems, you can replace URL)
1) Edit the source file. Let's insert an image URL into each open document.
The following command inserts (in case of problems, you can replace the URL)
`this.AddImageUrl(['http://localhost/example/images/logo.png']);`
after event
`this.sendEvent('asc_onDocumentContentReady');`
in file
`sdkjs/common/apiBase.js`
### change sdkjs on Windows (PowerShell)
```bash
(Get-Content sdkjs/common/apiBase.js) -replace "this\.sendEvent\('asc_onDocumentContentReady'\);", "this.sendEvent('asc_onDocumentContentReady');this.AddImageUrl(['http://localhost/example/images/logo.png']);" | Set-Content sdkjs/common/apiBase.js
**Windows (PowerShell):**
```powershell
(Get-Content sdkjs/common/apiBase.js) -replace "this\.sendEvent\('asc_onDocumentContentReady'\);", "this.sendEvent('asc_onDocumentContentReady');this.AddImageUrl(['http://localhost/example/images/logo.png']);" | Set-Content sdkjs/common/apiBase.js
```
### change sdkjs on Linux or macOS
**Linux:**
```bash
sed -i "s,this.sendEvent('asc_onDocumentContentReady');,this.sendEvent('asc_onDocumentContentReady');this.AddImageUrl(['http://localhost/example/images/logo.png']);," sdkjs/common/apiBase.js
```
2)Delete browser cache or hard reload the page `Ctrl + Shift + R`
**macOS:**
```bash
sed -i '' "s,this.sendEvent('asc_onDocumentContentReady');,this.sendEvent('asc_onDocumentContentReady');this.AddImageUrl(['http://localhost/example/images/logo.png']);," sdkjs/common/apiBase.js
```
3)Open new file in browser
2) Clear the browser cache or hard reload the page (`Ctrl + Shift + R` or `Cmd + Shift + R` on macOS)
### To change something in `server` do the following steps
3) Open a new file in the browser
1)Edit source file. Let's send `"Hello World!"`
chart message every time a document is opened.
Following command inserts
### To change something in `server`, do the following steps
1) Edit the source file. Let's send a `"Hello World!"`
chat message every time a document is opened.
The following command inserts
`yield* onMessage(ctx, conn, {"message": "Hello World!"});`
in function
`sendAuthInfo`
in file
`server/DocService/sources/DocsCoServer.js`
### change server on Windows (PowerShell)
```bash
**Windows (PowerShell):**
```powershell
(Get-Content server/DocService/sources/DocsCoServer.js) -replace 'opt_hasForgotten, opt_openedAt\) \{', 'opt_hasForgotten, opt_openedAt) {yield* onMessage(ctx, conn, {"message": "Hello World!"});' | Set-Content server/DocService/sources/DocsCoServer.js
```
### change server on Linux or macOS
**Linux:**
```bash
sed -i 's#opt_hasForgotten, opt_openedAt) {#opt_hasForgotten, opt_openedAt) {yield* onMessage(ctx, conn, {"message": "Hello World!"});#' server/DocService/sources/DocsCoServer.js
```
2)Restart document server process
**macOS:**
```bash
sed -i '' 's#opt_hasForgotten, opt_openedAt) {#opt_hasForgotten, opt_openedAt) {yield* onMessage(ctx, conn, {"message": "Hello World!"});#' server/DocService/sources/DocsCoServer.js
```
**Note**: Look for ``CONTAINER_ID`` in the result of ``docker ps``.
2) Restart the document server process
**Note**: Look for `CONTAINER_ID` in the result of `docker ps`.
```bash
docker exec -it CONTAINER_ID supervisorctl restart all
```
3)Open new file in browser
3) Open a new file in the browser
## Start server with additional functionality(addons)
## Start server with additional functionality (addons)
To get additional functionality and branding you need to connect a branding folder,
additional addon folders and pass command line arguments
To get additional functionality and branding, you need to connect a branding folder,
additional addon folders, and pass command line arguments.
For example run with `onlyoffice` branding and
addons:`sdkjs-forms`, `sdkjs-ooxml`, `web-apps-mobile`
For example, run with `onlyoffice` branding and
addons: `sdkjs-forms`, `sdkjs-ooxml`, `web-apps-mobile`.
### docker run on Windows (PowerShell) with branding
**Note**: Run PowerShell as administrator to fix EACCES error when installing
node_modules
node_modules.
```bash
```powershell
docker run -i -t -p 80:80 --restart=always -e ALLOW_PRIVATE_IP_ADDRESS=true `
-v $pwd/sdkjs:/var/www/onlyoffice/documentserver/sdkjs -v $pwd/web-apps:/var/www/onlyoffice/documentserver/web-apps `
-v $pwd/onlyoffice:/var/www/onlyoffice/documentserver/onlyoffice -v $pwd/sdkjs-ooxml:/var/www/onlyoffice/documentserver/sdkjs-ooxml -v $pwd/sdkjs-forms:/var/www/onlyoffice/documentserver/sdkjs-forms -v $pwd/web-apps-mobile:/var/www/onlyoffice/documentserver/web-apps-mobile `

View File

@ -68,8 +68,10 @@ def build_server_with_addons():
for addon in addons:
if (addon):
addon_dir = base.get_script_dir() + "/../../" + addon
if (base.is_exist(addon_dir)):
if (base.is_exist(addon_dir + "/package.json")):
base.print_info("npm ci: " + addon)
base.cmd_in_dir(addon_dir, "npm", ["ci"])
base.print_info("npm run build: " + addon)
base.cmd_in_dir(addon_dir, "npm", ["run", "build"])
def build_server_develop():

View File

@ -33,7 +33,7 @@ def make():
old_cur = os.getcwd()
os.chdir(base_dir)
base.common_check_version("socketio", "1", clean)
base.common_check_version("socketio", "2", clean)
os.chdir(old_cur)
if not base.is_dir(base_dir + "/socket.io-client-cpp"):

View File

@ -14,7 +14,7 @@ and Plugins (Methods/Events) API using the following Python scripts:
```bash
Node.js v20 and above
Python v3.10 and above
Python v3.12 and above
```
## Installation

View File

@ -4,6 +4,8 @@ import re
import shutil
import argparse
import generate_docs_json
import json
from pathlib import PurePosixPath
# Configuration files
editors = {
@ -20,9 +22,36 @@ root = os.path.abspath(os.path.join(os.path.dirname(script_path), '../../../../.
missing_examples = []
used_enumerations = set()
translations = {}
translations_lang = None
missed_translations = {}
used_translations_keys = {}
global_output_dir = ""
cur_editor_name = None
def find_common_path_part(path_full: str, path_suffix: str, anchor: str) -> str:
path_full = path_full.replace('\\', '/')
path_suffix = path_suffix.replace('\\', '/')
parts1 = PurePosixPath(path_full).parts
parts2 = PurePosixPath(path_suffix).parts
try:
idx1 = [p.lower() for p in parts1].index(anchor.lower())
idx2 = [p.lower() for p in parts2].index(anchor.lower())
except ValueError:
return ""
common_segments = []
for p1, p2 in zip(parts1[idx1:], parts2[idx2:]):
if p1.lower() == p2.lower():
common_segments.append(p1)
else:
break
return "/".join(common_segments)
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
@ -36,6 +65,21 @@ def remove_js_comments(text):
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # multi-line
return text.strip()
def get_translation(key):
def process_part(k):
if k not in translations:
missed_translations[k] = k
else:
used_translations_keys[k] = True
return translations.get(k, k)
if '\\\n' in key:
parts = key.split('\\\n')
translated_parts = [process_part(p) for p in parts]
return '\\\n'.join(translated_parts)
return process_part(key)
def process_link_tags(text, root=''):
"""
Finds patterns like {@link ...} and replaces them with Markdown links.
@ -43,27 +87,22 @@ def process_link_tags(text, root=''):
otherwise, a link to a class method is created.
For a method, if an alias is not specified, the name is left in the format 'Class#Method'.
"""
reserved_links = {
'/docbuilder/global#ShapeType': f"{'../../../../../../' if root == '' else '../../../../../' if root == '../' else root}docs/office-api/usage-api/text-document-api/Enumeration/ShapeType.md",
'/plugin/config': 'https://api.onlyoffice.com/docs/plugin-and-macros/structure/configuration/',
'/docbuilder/basic': 'https://api.onlyoffice.com/docs/office-api/usage-api/text-document-api/'
}
def replace_link(match):
content = match.group(1).strip() # Example: "/docbuilder/global#ShapeType shape type" or "global#ErrorValue ErrorValue"
content = match.group(1).strip() # Example: "global#ShapeType shape type" or "global#ErrorValue ErrorValue
parts = content.split()
ref = parts[0]
label = parts[1] if len(parts) > 1 else None
if ref.startswith('/'):
# Handle reserved links using mapping
if ref in reserved_links:
url = reserved_links[ref]
display_text = label if label else ref
return f"[{display_text}]({url})"
else:
# If the link is not in the mapping, return the original construction
return match.group(0)
if ref.startswith('/docs/'):
url = root + '../../../..' + ref
display_text = label if label else ref
if url.endswith('/'):
last_dir = url.rstrip('/').split('/')[-1]
url = f"{url}{last_dir}"
return f"[{display_text}]({url}.md)"
elif ref.startswith("global#"):
# Handle links to typedef (similar logic as before)
typedef_name = ref.split("#")[1]
@ -91,7 +130,7 @@ def correct_description(string, root='', isInTable=False):
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
return get_translation('No description provided.')
if False == isInTable:
# Line breaks
@ -100,6 +139,7 @@ def correct_description(string, root='', isInTable=False):
string = re.sub(r'<b>', '-**', string)
else:
string = re.sub(r'<b>', '**', string)
string = remove_line_breaks(string)
string = re.sub(r'</b>', '**', string)
@ -109,7 +149,7 @@ def correct_description(string, root='', isInTable=False):
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
return get_translation(string)
def correct_default_value(value, enumerations, classes):
if value is None or value == '':
@ -306,12 +346,12 @@ def generate_data_types_markdown(types, enumerations, classes, root='../../'):
def generate_class_markdown(class_name, methods, properties, enumerations, classes):
content = f"# {class_name}\n\nRepresents the {class_name} class.\n\n"
content = f"# {class_name}\n\n{get_translation(f"Represents the {class_name} class.")}\n\n"
content += generate_properties_markdown(properties, enumerations, classes)
content += "\n## Methods\n\n"
content += "| Method | Returns | Description |\n"
content += f"\n## {get_translation(f"Methods")}\n\n"
content += f"| {get_translation(f"Method")} | {get_translation(f"Returns")} | {get_translation(f"Description")} |\n"
content += "| ------ | ------- | ----------- |\n"
for method in sorted(methods, key=lambda m: m['name']):
@ -323,10 +363,10 @@ def generate_class_markdown(class_name, methods, properties, enumerations, class
return_type_list = returns[0].get('type', {}).get('names', [])
returns_markdown = generate_data_types_markdown(return_type_list, enumerations, classes, '../')
else:
returns_markdown = "None"
returns_markdown = get_translation(f"None")
# Processing the method description
description = remove_line_breaks(correct_description(method.get('description', 'No description provided.'), '../', True))
description = correct_description(method.get('description', 'No description provided.'), '../', True)
# Form a link to the method document
method_link = f"[{method_name}](./Methods/{method_name}.md)"
@ -348,47 +388,47 @@ def generate_method_markdown(method, enumerations, classes, example_editor_name)
# Syntax
param_list = ', '.join([param['name'] for param in params if '.' not in param['name']]) if params else ''
content += f"## Syntax\n\n```javascript\nexpression.{method_name}({param_list});\n```\n\n"
content += f"## {get_translation(f"Syntax")}\n\n```javascript\nexpression.{method_name}({param_list});\n```\n\n"
if memberof:
content += f"`expression` - A variable that represents a [{memberof}](../{memberof}.md) class.\n\n"
content += f"`expression` - {get_translation(f"A variable that represents a [{memberof}](../{memberof}.md) class.")}\n\n"
# Parameters
content += "## Parameters\n\n"
content += f"## {get_translation(f"Parameters")}\n\n"
if params:
content += "| **Name** | **Required/Optional** | **Data type** | **Default** | **Description** |\n"
content += f"| **{get_translation(f"Name")}** | **{get_translation(f"Required/Optional")}** | **{get_translation(f"Data type")}** | **{get_translation(f"Default")}** | **{get_translation(f"Description")}** |\n"
content += "| ------------- | ------------- | ------------- | ------------- | ------------- |\n"
for param in params:
param_name = param.get('name', 'Unnamed')
param_types = param.get('type', {}).get('names', []) if param.get('type') else []
param_types_md = generate_data_types_markdown(param_types, enumerations, classes)
param_desc = remove_line_breaks(correct_description(param.get('description', 'No description provided.'), '../../', True))
param_required = "Required" if not param.get('optional') else "Optional"
param_desc = correct_description(param.get('description', 'No description provided.'), '../../', True)
param_required = f"{get_translation(f"Required")}" if not param.get('optional') else f"{get_translation(f"Optional")}"
param_default = correct_default_value(param.get('defaultvalue', ''), enumerations, classes)
content += f"| {param_name} | {param_required} | {param_types_md} | {param_default} | {param_desc} |\n"
else:
content += "This method doesn't have any parameters.\n"
content += f"{get_translation("This method doesn't have any parameters.")}\n"
# Returns
content += "\n## Returns\n\n"
content += f"\n## {get_translation(f"Returns")}\n\n"
if returns:
return_type_list = returns[0].get('type', {}).get('names', [])
return_type_md = generate_data_types_markdown(return_type_list, enumerations, classes)
content += return_type_md
else:
content += "This method doesn't return any data."
content += get_translation(f"This method doesn't return any data.")
# Example
if example:
# Separate comment and code, remove JS comments
if '```js' in example:
comment, code = example.split('```js', 1)
comment = remove_js_comments(comment)
content += f"\n\n## Example\n\n{comment}\n\n```javascript {example_editor_name}\n{code.strip()}\n"
comment = get_translation(comment.strip())
content += f"\n\n## {get_translation(f"Example")}\n\n{comment}\n\n```javascript {example_editor_name}\n{code.strip()}\n"
else:
# If there's no triple-backtick structure, just show it as code
cleaned_example = remove_js_comments(example)
content += f"\n\n## Example\n\n```javascript {example_editor_name}\n{cleaned_example}\n```\n"
content += f"\n\n## {get_translation(f"Example")}\n\n```javascript {example_editor_name}\n{cleaned_example}\n```\n"
return escape_text_outside_code_blocks(content)
@ -396,14 +436,14 @@ def generate_properties_markdown(properties, enumerations, classes, root='../'):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | Description |\n"
content = f"## {get_translation(f"Properties")}\n\n"
content += f"| {get_translation(f"Name")} | {get_translation(f"Type")} | {get_translation(f"Description")} |\n"
content += "| ---- | ---- | ----------- |\n"
for prop in sorted(properties, key=lambda m: m['name']):
prop_name = prop['name']
prop_description = prop.get('description', 'No description provided.')
prop_description = remove_line_breaks(correct_description(prop_description, root, True))
prop_description = correct_description(prop_description, root, True)
prop_types = prop['type']['names'] if prop.get('type') else []
param_types_md = generate_data_types_markdown(prop_types, enumerations, classes, root)
content += f"| {prop_name} | {param_types_md} | {prop_description} |\n"
@ -427,8 +467,8 @@ def generate_enumeration_markdown(enumeration, enumerations, classes, example_ed
if ptype['type'] == 'TypeUnion':
enum_empty = True # is empty enum
content += "## Type\n\nEnumeration\n\n"
content += "## Values\n\n"
content += f"## {get_translation(f"Type")}\n\n{get_translation(f"Enumeration")}\n\n"
content += f"## {get_translation(f"Values")}\n\n"
# Each top-level name in the union
for raw_t in enumeration['type']['names']:
ts_t = convert_jsdoc_array_to_ts(raw_t)
@ -448,25 +488,25 @@ def generate_enumeration_markdown(enumeration, enumerations, classes, example_ed
if enum_empty == True:
return None
elif enumeration['properties'] is not None:
content += "## Type\n\nObject\n\n"
content += f"## {get_translation(f"Type")}\n\n{get_translation(f"Object")}\n\n"
content += generate_properties_markdown(enumeration['properties'], enumerations, classes)
else:
content += "## Type\n\n"
content += f"## {get_translation(f"Type")}\n\n"
# If it's not a union and has no properties, simply print the type(s).
types = enumeration['type']['names']
t_md = generate_data_types_markdown(types, enumerations, classes)
t_md = generate_data_types_markdown(types, enumerations, classes, '../')
content += t_md + "\n\n"
# Example
if example:
if '```js' in example:
comment, code = example.split('```js', 1)
comment = remove_js_comments(comment)
content += f"\n\n## Example\n\n{comment}\n\n```javascript {example_editor_name}\n{code.strip()}\n"
comment = get_translation(comment.strip())
content += f"\n\n## {get_translation(f"Example")}\n\n{comment}\n\n```javascript {example_editor_name}\n{code.strip()}\n"
else:
# If there's no triple-backtick structure
cleaned_example = remove_js_comments(example)
content += f"\n\n## Example\n\n```javascript {example_editor_name}\n{cleaned_example}\n```\n"
content += f"\n\n## {get_translation(f"Example")}\n\n```javascript {example_editor_name}\n{cleaned_example}\n```\n"
return escape_text_outside_code_blocks(content)
@ -556,7 +596,18 @@ def process_doclets(data, output_dir, editor_name):
if not enum.get('example', ''):
missing_examples.append(os.path.relpath(enum_file_path, output_dir))
def generate(output_dir):
def generate(output_dir, translations_file):
global translations
global translations_lang
global global_output_dir
global_output_dir = output_dir
if translations_file is not None and os.path.exists(translations_file):
translations = load_json(translations_file)
translations_lang = os.path.splitext(os.path.basename(translations_file))[0]
else:
translations = {}
os.chdir(os.path.dirname(script_path))
print('Generating Markdown documentation...')
@ -575,6 +626,21 @@ def generate(output_dir):
used_enumerations.clear()
process_doclets(data, output_dir, editor_name)
if translations_file is not None:
target_dir = os.path.dirname(translations_file)
missed_file_path = os.path.join(target_dir, "missed_translations.json")
print(f'Saving missed translations to: {missed_file_path}')
with open(missed_file_path, 'w', encoding='utf-8') as f:
json.dump(missed_translations, f, ensure_ascii=False, indent=4)
unused_keys = set(translations.keys()) - set(used_translations_keys.keys())
unused_data = {k: translations[k] for k in unused_keys}
unused_file_path = os.path.join(target_dir, "unused_translations.json")
print(f'Saving unused translations to: {unused_file_path}')
with open(unused_file_path, 'w', encoding='utf-8') as f:
json.dump(unused_data, f, ensure_ascii=False, indent=4)
shutil.rmtree(output_dir + 'tmp_json')
print('Done')
@ -587,8 +653,17 @@ if __name__ == "__main__":
nargs='?', # Indicates the argument is optional
default=f"{root}/api.onlyoffice.com/site/docs/office-api/usage-api/" # Default value
)
parser.add_argument(
"--translations",
type=str,
help="Path to the JSON file with translations",
nargs='?',
default=None
)
args = parser.parse_args()
generate(args.destination)
generate(args.destination, args.translations)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -5,6 +5,8 @@ import re
import shutil
import argparse
import generate_docs_events_json
import json
from pathlib import PurePosixPath
# Папки для каждого editor_name
editors = {
@ -20,7 +22,34 @@ root = os.path.abspath(os.path.join(os.path.dirname(script_path), '../../../../.
missing_examples = []
used_enumerations = set()
translations = {}
translations_lang = None
missed_translations = {}
used_translations_keys = {}
global_output_dir = ""
def find_common_path_part(path_full: str, path_suffix: str, anchor: str) -> str:
path_full = path_full.replace('\\', '/')
path_suffix = path_suffix.replace('\\', '/')
parts1 = PurePosixPath(path_full).parts
parts2 = PurePosixPath(path_suffix).parts
try:
idx1 = [p.lower() for p in parts1].index(anchor.lower())
idx2 = [p.lower() for p in parts2].index(anchor.lower())
except ValueError:
return ""
common_segments = []
for p1, p2 in zip(parts1[idx1:], parts2[idx2:]):
if p1.lower() == p2.lower():
common_segments.append(p1)
else:
break
return "/".join(common_segments)
def load_json(path):
with open(path, 'r', encoding='utf-8') as f:
@ -38,6 +67,20 @@ def remove_js_comments(text):
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
return text.strip()
def get_translation(key):
def process_part(k):
if k not in translations:
missed_translations[k] = k
else:
used_translations_keys[k] = True
return translations.get(k, k)
if '\\\n' in key:
parts = key.split('\\\n')
translated_parts = [process_part(p) for p in parts]
return '\\\n'.join(translated_parts)
return process_part(key)
def correct_description(string, root='', isInTable=False):
"""
@ -49,7 +92,7 @@ def correct_description(string, root='', isInTable=False):
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
return get_translation('No description provided.')
if False == isInTable:
# Line breaks
@ -58,6 +101,7 @@ def correct_description(string, root='', isInTable=False):
string = re.sub(r'<b>', '-**', string)
else:
string = re.sub(r'<b>', '**', string)
string = remove_line_breaks(string)
string = re.sub(r'</b>', '**', string)
@ -67,7 +111,7 @@ def correct_description(string, root='', isInTable=False):
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
return get_translation(string)
def process_link_tags(text, root=''):
"""
@ -76,31 +120,22 @@ def process_link_tags(text, root=''):
otherwise, a link to a class method is created.
For a method, if an alias is not specified, the name is left in the format 'Class#Method'.
"""
reserved_links = {
'/docbuilder/global#ShapeType': f"{'../../../../../../' if root == '' else '../../../../../' if root == '../' else root}docs/office-api/usage-api/text-document-api/Enumeration/ShapeType.md",
'/plugin/config': 'https://api.onlyoffice.com/docs/plugin-and-macros/structure/configuration/',
'/docbuilder/basic': 'https://api.onlyoffice.com/docs/office-api/usage-api/text-document-api/'
}
def replace_link(match):
content = match.group(1).strip() # Example: "/docbuilder/global#ShapeType shape type" or "global#ErrorValue ErrorValue"
content = match.group(1).strip() # Example: "global#ShapeType shape type" or "global#ErrorValue ErrorValue
parts = content.split()
ref = parts[0]
label = parts[1] if len(parts) > 1 else None
if ref.startswith('/'):
# Handle reserved links using mapping
if ref in reserved_links:
url = reserved_links[ref]
display_text = label if label else ref
return f"[{display_text}]({url})"
elif ref.startswith('/docs/plugins/'):
url = f"../../{ref.split('/docs/plugins/')[1]}.md"
display_text = label if label else ref
return f"[{display_text}]({url})"
else:
# If the link is not in the mapping, return the original construction
return match.group(0)
if ref.startswith('/docs/'):
url = root + '../../../..' + ref
display_text = label if label else ref
if url.endswith('/'):
last_dir = url.rstrip('/').split('/')[-1]
url = f"{url}{last_dir}"
return f"[{display_text}]({url}.md)"
elif ref.startswith("global#"):
# Handle links to typedef (similar logic as before)
typedef_name = ref.split("#")[1]
@ -161,26 +196,26 @@ def escape_text_outside_code_blocks(md):
def generate_event_markdown(event, enumerations):
name = event['name']
desc = correct_description(event.get('description', ''))
desc = correct_description(event.get('description', ''), '../', True)
params = event.get('params', [])
md = f"# {name}\n\n{desc}\n\n"
# Parameters
md += "## Parameters\n\n"
md += f"## {get_translation("Parameters")}\n\n"
if params:
md += "| **Name** | **Data type** | **Description** |\n"
md += f"| **{get_translation("Name")}** | **{get_translation("Data type")}** | **{get_translation("Description")}** |\n"
md += "| --------- | ------------- | ----------- |\n"
for p in params:
t_md = generate_data_types_markdown(
p.get('type', {}).get('names', []),
enumerations
)
d = remove_line_breaks(correct_description(p.get('description', ''), isInTable=True))
d = correct_description(p.get('description', ''), isInTable=True)
md += f"| {p['name']} | {t_md} | {d} |\n"
md += "\n"
else:
md += "This event has no parameters.\n\n"
md += f"{get_translation("This event has no parameters.")}\n\n"
for ex in event.get('examples', []):
code = remove_js_comments(ex).strip()
@ -213,7 +248,7 @@ def generate_enumeration_markdown(enumeration, enumerations):
# If parsedType is missing, just list 'type.names' if available
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## Type\n\n"
content += f"## {get_translation("Type")}\n\n"
t_md = generate_data_types_markdown(type_names, enumerations)
content += t_md + "\n\n"
else:
@ -221,8 +256,8 @@ def generate_enumeration_markdown(enumeration, enumerations):
# 1) Handle TypeUnion
if ptype == 'TypeUnion':
content += "## Type\n\nEnumeration\n\n"
content += "## Values\n\n"
content += f"## {get_translation("Type")}\n\n{get_translation("Enumeration")}\n\n"
content += f"## {get_translation("Values")}\n\n"
for raw_t in enumeration['type']['names']:
# Attempt linking
if any(enum['name'] == raw_t for enum in enumerations):
@ -233,11 +268,11 @@ def generate_enumeration_markdown(enumeration, enumerations):
# 2) Handle TypeApplication (e.g. Object.<string, string>)
elif ptype == 'TypeApplication':
content += "## Type\n\nObject\n\n"
content += f"## {get_translation("Type")}\n\n{get_translation("Object")}\n\n"
type_names = enumeration['type'].get('names', [])
if type_names:
t_md = generate_data_types_markdown(type_names, enumerations)
content += f"**Type:** {t_md}\n\n"
content += f"**{get_translation("Type")}:** {t_md}\n\n"
# 3) If properties are present, treat it like an object
if enumeration.get('properties') is not None:
@ -247,16 +282,16 @@ def generate_enumeration_markdown(enumeration, enumerations):
if ptype not in ('TypeUnion', 'TypeApplication'):
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## Type\n\n"
content += f"## {get_translation("Type")}\n\n"
t_md = generate_data_types_markdown(type_names, enumerations)
content += t_md + "\n\n"
# Process examples array
if examples:
if len(examples) > 1:
content += "\n\n## Examples\n\n"
content += f"\n\n## {get_translation("Examples")}\n\n"
else:
content += "\n\n## Example\n\n"
content += f"\n\n## {get_translation("Example")}\n\n"
for i, ex_line in enumerate(examples, start=1):
# Remove JS comments
@ -265,15 +300,15 @@ def generate_enumeration_markdown(enumeration, enumerations):
# Attempt splitting if the user used ```js
if '```js' in cleaned_example:
comment, code = cleaned_example.split('```js', 1)
comment = comment.strip()
comment = get_translation(comment.strip())
code = code.strip()
if len(examples) > 1:
content += f"**Example {i}:**\n\n{comment}\n\n"
content += f"**{get_translation("Example")} {i}:**\n\n{comment}\n\n"
content += f"```javascript\n{code}\n```\n"
else:
if len(examples) > 1:
content += f"**Example {i}:**\n\n{comment}\n\n"
content += f"**{get_translation("Example")} {i}:**\n\n{comment}\n\n"
# No special fences, just show as code
content += f"```javascript\n{cleaned_example}\n```\n"
@ -284,13 +319,13 @@ def generate_events_summary(events):
Create Events.md summary listing all events with their description.
"""
header = [
"# Events\n\n",
"| Event | Description |\n",
f"# {get_translation("Events")}\n\n",
f"| {get_translation("Event")} | {get_translation("Description")} |\n",
"| ----- | ----------- |\n"
]
lines = [
f"| [{ev['name']}](./{ev['name']}.md) | "
f"{remove_line_breaks(correct_description(ev.get('description', ''), isInTable=True))} |\n"
f"{correct_description(ev.get('description', ''), '../', isInTable=True)} |\n"
for ev in sorted(events, key=lambda e: e['name'])
]
return "".join(header + lines)
@ -299,14 +334,14 @@ def generate_properties_markdown(properties, enumerations):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | Description |\n"
content = f"## {get_translation("Properties")}\n\n"
content += f"| {get_translation("Name")} | {get_translation("Type")} | {get_translation("Description")} |\n"
content += "| ---- | ---- | ----------- |\n"
for prop in sorted(properties, key=lambda m: m['name']):
prop_name = prop['name']
prop_description = prop.get('description', 'No description provided.')
prop_description = remove_line_breaks(correct_description(prop_description, isInTable=True))
prop_description = correct_description(prop_description, isInTable=True)
prop_types = prop['type']['names'] if prop.get('type') else []
param_types_md = generate_data_types_markdown(prop_types, enumerations)
content += f"| {prop_name} | {param_types_md} | {prop_description} |\n"
@ -375,7 +410,18 @@ def process_events(data, editor_dir):
# events summary
write_markdown_file(os.path.join(events_dir, "Events.md"), generate_events_summary(events))
def generate_events(output_dir):
def generate_events(output_dir, translations_file):
global translations
global translations_lang
global global_output_dir
global_output_dir = output_dir
if translations_file is not None and os.path.exists(translations_file):
translations = load_json(translations_file)
translations_lang = os.path.splitext(os.path.basename(translations_file))[0]
else:
translations = {}
os.chdir(os.path.dirname(script_path))
if output_dir.endswith('/'):
@ -388,6 +434,21 @@ def generate_events(output_dir):
data = load_json(os.path.join(tmp, f"{editor_name}.json"))
process_events(data, os.path.join(output_dir, folder))
if translations_file is not None:
target_dir = os.path.dirname(translations_file)
missed_file_path = os.path.join(target_dir, "missed_translations.json")
print(f'Saving missed translations to: {missed_file_path}')
with open(missed_file_path, 'w', encoding='utf-8') as f:
json.dump(missed_translations, f, ensure_ascii=False, indent=4)
unused_keys = set(translations.keys()) - set(used_translations_keys.keys())
unused_data = {k: translations[k] for k in unused_keys}
unused_file_path = os.path.join(target_dir, "unused_translations.json")
print(f'Saving unused translations to: {unused_file_path}')
with open(unused_file_path, 'w', encoding='utf-8') as f:
json.dump(unused_data, f, ensure_ascii=False, indent=4)
shutil.rmtree(tmp)
print("Done. Missing examples:", missing_examples)
@ -400,5 +461,14 @@ if __name__ == "__main__":
default=f"{root}/api.onlyoffice.com/site/docs/plugin-and-macros/interacting-with-editors/",
help="Output directory"
)
parser.add_argument(
"--translations",
type=str,
help="Path to the JSON file with translations",
nargs='?',
default=None
)
args = parser.parse_args()
generate_events(args.destination)
generate_events(args.destination, args.translations)

View File

@ -4,6 +4,8 @@ import re
import shutil
import argparse
import generate_docs_methods_json
import json
from pathlib import PurePosixPath
# Configuration files
editors = {
@ -19,9 +21,36 @@ root = os.path.abspath(os.path.join(os.path.dirname(script_path), '../../../../.
missing_examples = []
used_enumerations = set()
translations = {}
translations_lang = None
missed_translations = {}
used_translations_keys = {}
global_output_dir = ""
cur_editor_name = None
def find_common_path_part(path_full: str, path_suffix: str, anchor: str) -> str:
path_full = path_full.replace('\\', '/')
path_suffix = path_suffix.replace('\\', '/')
parts1 = PurePosixPath(path_full).parts
parts2 = PurePosixPath(path_suffix).parts
try:
idx1 = [p.lower() for p in parts1].index(anchor.lower())
idx2 = [p.lower() for p in parts2].index(anchor.lower())
except ValueError:
return ""
common_segments = []
for p1, p2 in zip(parts1[idx1:], parts2[idx2:]):
if p1.lower() == p2.lower():
common_segments.append(p1)
else:
break
return "/".join(common_segments)
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
@ -35,6 +64,21 @@ def remove_js_comments(text):
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # multi-line
return text.strip()
def get_translation(key):
def process_part(k):
if k not in translations:
missed_translations[k] = k
else:
used_translations_keys[k] = True
return translations.get(k, k)
if '\\\n' in key:
parts = key.split('\\\n')
translated_parts = [process_part(p) for p in parts]
return '\\\n'.join(translated_parts)
return process_part(key)
def process_link_tags(text, root=''):
"""
Finds patterns like {@link ...} and replaces them with Markdown links.
@ -42,36 +86,28 @@ def process_link_tags(text, root=''):
otherwise, a link to a class method is created.
For a method, if an alias is not specified, the name is left in the format 'Class#Method'.
"""
reserved_links = {
'/docbuilder/global#ShapeType': f"{'../../../../../' if root == '' else '../../../../' if root == '../' else root}docs/office-api/usage-api/text-document-api/Enumeration/ShapeType.md",
'/plugin/config': 'https://api.onlyoffice.com/docs/plugin-and-macros/structure/configuration/',
'/docbuilder/basic': 'https://api.onlyoffice.com/docs/office-api/usage-api/text-document-api/'
}
def replace_link(match):
content = match.group(1).strip() # Example: "/docbuilder/global#ShapeType shape type" or "global#ErrorValue ErrorValue"
content = match.group(1).strip() # Example: "global#ShapeType shape type" or "global#ErrorValue ErrorValue
parts = content.split()
ref = parts[0]
label = parts[1] if len(parts) > 1 else None
if ref.startswith('/'):
# Handle reserved links using mapping
if ref in reserved_links:
url = reserved_links[ref]
display_text = label if label else ref
return f"[{display_text}]({url})"
else:
# If the link is not in the mapping, return the original construction
return match.group(0)
if ref.startswith('/docs/'):
url = root + '../../../..' + ref
display_text = label if label else ref
if url.endswith('/'):
last_dir = url.rstrip('/').split('/')[-1]
url = f"{url}{last_dir}"
return f"[{display_text}]({url}.md)"
elif ref.startswith("global#"):
# Handle links to typedef (similar logic as before)
typedef_name = ref.split("#")[1]
used_enumerations.add(typedef_name)
display_text = label if label else typedef_name
return f"[{display_text}]({root}Enumeration/{typedef_name}.md)"
elif ref.startswith("https"):
display_text = label if label else ref # Keep the full notation, e.g., "Api#CreateSlide"
return f"[{display_text}]({ref})"
else:
# Handle links to class methods like ClassName#MethodName
try:
@ -93,7 +129,7 @@ def correct_description(string, root='', isInTable=False):
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
return get_translation('No description provided.')
if False == isInTable:
# Line breaks
@ -102,6 +138,7 @@ def correct_description(string, root='', isInTable=False):
string = re.sub(r'<b>', '-**', string)
else:
string = re.sub(r'<b>', '**', string)
string = remove_line_breaks(string)
string = re.sub(r'</b>', '**', string)
@ -111,7 +148,7 @@ def correct_description(string, root='', isInTable=False):
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
return get_translation(string)
def correct_default_value(value, enumerations, classes):
if value is None or value == '':
@ -309,12 +346,12 @@ def generate_data_types_markdown(types, enumerations, classes, root='../'):
return param_types_md
def generate_class_markdown(class_name, methods, properties, enumerations, classes):
content = f"# {class_name}\n\nRepresents the {class_name} class.\n\n"
content = f"# {class_name}\n\n{get_translation(f"Represents the {class_name} class.")}\n\n"
content += generate_properties_markdown(properties, enumerations, classes)
content += "\n## Methods\n\n"
content += "| Method | Returns | Description |\n"
content += f"\n## {get_translation("Methods")}\n\n"
content += f"| {get_translation("Method")} | {get_translation("Returns")} | {get_translation("Description")} |\n"
content += "| ------ | ------- | ----------- |\n"
for method in sorted(methods, key=lambda m: m['name']):
@ -326,10 +363,10 @@ def generate_class_markdown(class_name, methods, properties, enumerations, class
return_type_list = returns[0].get('type', {}).get('names', [])
returns_markdown = generate_data_types_markdown(return_type_list, enumerations, classes, '../')
else:
returns_markdown = "None"
returns_markdown = get_translation("None")
# Processing the method description
description = remove_line_breaks(correct_description(method.get('description', 'No description provided.'), '../', True))
description = correct_description(method.get('description', 'No description provided.'), '../', True)
# Form a link to the method document
method_link = f"[{method_name}](./{method_name}.md)"
@ -358,42 +395,42 @@ def generate_method_markdown(method, enumerations, classes):
# Syntax
param_list = ', '.join([param['name'] for param in params if '.' not in param['name']]) if params else ''
content += f"## Syntax\n\n```javascript\nexpression.{method_name}({param_list});\n```\n\n"
content += f"## {get_translation("Syntax")}\n\n```javascript\nexpression.{method_name}({param_list});\n```\n\n"
if memberof:
content += f"`expression` - A variable that represents a [{memberof}](Methods.md) class.\n\n"
content += f"`expression` - {get_translation(f"A variable that represents a [{memberof}](Methods.md) class.")}\n\n"
# Parameters
content += "## Parameters\n\n"
content += f"## {get_translation("Parameters")}\n\n"
if params:
content += "| **Name** | **Required/Optional** | **Data type** | **Default** | **Description** |\n"
content += f"| **{get_translation("Name")}** | **{get_translation("Required/Optional")}** | **{get_translation("Data type")}** | **{get_translation("Default")}** | **{get_translation("Description")}** |\n"
content += "| ------------- | ------------- | ------------- | ------------- | ------------- |\n"
for param in params:
param_name = param.get('name', 'Unnamed')
param_types = param.get('type', {}).get('names', []) if param.get('type') else []
param_types_md = generate_data_types_markdown(param_types, enumerations, classes)
param_desc = remove_line_breaks(correct_description(param.get('description', 'No description provided.'), '../', True))
param_required = "Required" if not param.get('optional') else "Optional"
param_desc = correct_description(param.get('description', 'No description provided.'), '../', True)
param_required = get_translation("Required") if not param.get('optional') else get_translation("Optional")
param_default = correct_default_value(param.get('defaultvalue', ''), enumerations, classes)
content += f"| {param_name} | {param_required} | {param_types_md} | {param_default} | {param_desc} |\n"
else:
content += "This method doesn't have any parameters.\n"
content += f"{get_translation("This method doesn't have any parameters.")}\n"
# Returns
content += "\n## Returns\n\n"
content += f"\n## {get_translation("Returns")}\n\n"
if returns:
return_type_list = returns[0].get('type', {}).get('names', [])
return_type_md = generate_data_types_markdown(return_type_list, enumerations, classes)
content += return_type_md
else:
content += "This method doesn't return any data."
content += get_translation("This method doesn't return any data.")
# Process examples array
if examples:
if len(examples) > 1:
content += "\n\n## Examples\n\n"
content += f"\n\n## {get_translation("Examples")}\n\n"
else:
content += "\n\n## Example\n\n"
content += f"\n\n## {get_translation("Example")}\n\n"
for i, ex_line in enumerate(examples, start=1):
# Remove JS comments
@ -402,15 +439,15 @@ def generate_method_markdown(method, enumerations, classes):
# Attempt splitting if the user used ```js
if '```js' in cleaned_example:
comment, code = cleaned_example.split('```js', 1)
comment = comment.strip()
comment = get_translation(comment.strip())
code = code.strip()
if len(examples) > 1:
content += f"**Example {i}:**\n\n{comment}\n\n"
content += f"**{get_translation("Example")} {i}:**\n\n{comment}\n\n"
content += f"```javascript\n{code}\n```\n"
else:
if len(examples) > 1:
content += f"**Example {i}:**\n\n{comment}\n\n"
content += f"**{get_translation("Example")} {i}:**\n\n{comment}\n\n"
# No special fences, just show as code
content += f"```javascript\n{cleaned_example}\n```\n"
@ -420,14 +457,14 @@ def generate_properties_markdown(properties, enumerations, classes, root='../'):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | Description |\n"
content = f"## {get_translation("Properties")}\n\n"
content += f"| {get_translation("Name")} | {get_translation("Type")} | {get_translation("Description")} |\n"
content += "| ---- | ---- | ----------- |\n"
for prop in sorted(properties, key=lambda m: m['name']):
prop_name = prop['name']
prop_description = prop.get('description', 'No description provided.')
prop_description = remove_line_breaks(correct_description(prop_description, isInTable=True))
prop_description = correct_description(prop_description, root, isInTable=True)
prop_types = prop['type']['names'] if prop.get('type') else []
param_types_md = generate_data_types_markdown(prop_types, enumerations, classes, root)
content += f"| {prop_name} | {param_types_md} | {prop_description} |\n"
@ -459,7 +496,7 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
# If parsedType is missing, just list 'type.names' if available
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## Type\n\n"
content += f"## {get_translation("Type")}\n\n"
t_md = generate_data_types_markdown(type_names, enumerations, classes)
content += t_md + "\n\n"
else:
@ -467,8 +504,8 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
# 1) Handle TypeUnion
if ptype == 'TypeUnion':
content += "## Type\n\nEnumeration\n\n"
content += "## Values\n\n"
content += f"## {get_translation("Type")}\n\n{get_translation("Enumeration")}\n\n"
content += f"## {get_translation("Values")}\n\n"
for raw_t in enumeration['type']['names']:
# Attempt linking
if any(enum['name'] == raw_t for enum in enumerations):
@ -481,11 +518,11 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
# 2) Handle TypeApplication (e.g. Object.<string, string>)
elif ptype == 'TypeApplication':
content += "## Type\n\nObject\n\n"
content += f"## {get_translation("Type")}\n\n{get_translation("Object")}\n\n"
type_names = enumeration['type'].get('names', [])
if type_names:
t_md = generate_data_types_markdown(type_names, enumerations, classes)
content += f"**Type:** {t_md}\n\n"
content += f"**{get_translation("Type")}:** {t_md}\n\n"
# 3) If properties are present, treat it like an object
if enumeration.get('properties') is not None:
@ -495,16 +532,16 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
if ptype not in ('TypeUnion', 'TypeApplication'):
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## Type\n\n"
content += f"## {get_translation("Type")}\n\n"
t_md = generate_data_types_markdown(type_names, enumerations, classes)
content += t_md + "\n\n"
# Process examples array
if examples:
if len(examples) > 1:
content += "\n\n## Examples\n\n"
content += f"\n\n## {get_translation("Examples")}\n\n"
else:
content += "\n\n## Example\n\n"
content += f"\n\n## {get_translation("Example")}\n\n"
for i, ex_line in enumerate(examples, start=1):
# Remove JS comments
@ -513,15 +550,15 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
# Attempt splitting if the user used ```js
if '```js' in cleaned_example:
comment, code = cleaned_example.split('```js', 1)
comment = comment.strip()
comment = get_translation(comment.strip())
code = code.strip()
if len(examples) > 1:
content += f"**Example {i}:**\n\n{comment}\n\n"
content += f"**{get_translation("Example")} {i}:**\n\n{comment}\n\n"
content += f"```javascript\n{code}\n```\n"
else:
if len(examples) > 1:
content += f"**Example {i}:**\n\n{comment}\n\n"
content += f"**{get_translation("Example")} {i}:**\n\n{comment}\n\n"
# No special fences, just show as code
content += f"```javascript\n{cleaned_example}\n```\n"
@ -617,7 +654,18 @@ def process_doclets(data, output_dir, editor_name):
if not enum.get('examples', ''):
missing_examples.append(os.path.relpath(enum_file_path, output_dir))
def generate(output_dir):
def generate(output_dir, translations_file):
global translations
global translations_lang
global global_output_dir
global_output_dir = output_dir
if translations_file is not None and os.path.exists(translations_file):
translations = load_json(translations_file)
translations_lang = os.path.splitext(os.path.basename(translations_file))[0]
else:
translations = {}
os.chdir(os.path.dirname(script_path))
print('Generating Markdown documentation...')
@ -633,6 +681,21 @@ def generate(output_dir):
used_enumerations.clear()
process_doclets(data, output_dir, editor_name)
if translations_file is not None:
target_dir = os.path.dirname(translations_file)
missed_file_path = os.path.join(target_dir, "missed_translations.json")
print(f'Saving missed translations to: {missed_file_path}')
with open(missed_file_path, 'w', encoding='utf-8') as f:
json.dump(missed_translations, f, ensure_ascii=False, indent=4)
unused_keys = set(translations.keys()) - set(used_translations_keys.keys())
unused_data = {k: translations[k] for k in unused_keys}
unused_file_path = os.path.join(target_dir, "unused_translations.json")
print(f'Saving unused translations to: {unused_file_path}')
with open(unused_file_path, 'w', encoding='utf-8') as f:
json.dump(unused_data, f, ensure_ascii=False, indent=4)
shutil.rmtree(output_dir + '/tmp_json')
print('Done')
@ -645,8 +708,17 @@ if __name__ == "__main__":
nargs='?', # Indicates the argument is optional
default=f"{root}/api.onlyoffice.com/site/docs/plugin-and-macros/interacting-with-editors/" # Default value
)
parser.add_argument(
"--translations",
type=str,
help="Path to the JSON file with translations",
nargs='?',
default=None
)
args = parser.parse_args()
generate(args.destination)
generate(args.destination, args.translations)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python
import sys
sys.path.append('../../scripts')
import base
import os
import glob
from pathlib import Path
params = sys.argv[1:]
if (3 != len(params)):
print("use: thumbnails_auto.py path_to_builder_directory path_to_input_files_directory path_to_output_files_directory")
exit(0)
base.configure_common_apps()
directory_x2t = params[0].replace("\\", "/")
directory_input = params[1].replace("\\", "/")
directory_output = params[2].replace("\\", "/")
if not os.path.exists(directory_output):
os.mkdir(directory_output)
def rename_dir(input, output):
if base.is_dir(u"" + directory_output + u"/" + output):
base.delete_dir(u"" + directory_output + u"/" + output)
os.rename(u"" + directory_output + u"/" + input, u"" + directory_output + u"/" + output)
return
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "512", "724"])
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "1024", "1448"])
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "324", "458"])
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "648", "916"])
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "256", "368"])
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "400", "566"])
base.cmd("python", ["thumbnails_old.py", directory_x2t, directory_input, directory_output, "184", "260"])
#base.cmd("python", ["thumbnails.py", directory_x2t, directory_input, directory_output, "792", "1098"])
#rename_dir("[512x724]", "inside_1x_[512x724]")
#rename_dir("[1024x1448]", "inside_2x_[1024x1448]")
#rename_dir("[228x316]", "main_1x_[228x316]")
#rename_dir("[456x632]", "main_2x_[456x632]")
#rename_dir("[256x368]", "mobile_[256x368]")
#rename_dir("[792x1098]", "source_[792x1098]")
dirnames = list(Path(directory_output).iterdir())
for dir_name in dirnames:
if len(list(Path(dir_name).iterdir())) == 0:
print("Delete dir ", dir_name)
Path(dir_name).rmdir()
#base.delete_dir(dir_name)

View File

@ -0,0 +1,274 @@
#!/usr/bin/env python
import sys
sys.path.append('../../scripts')
import base
import os
import glob
import imagesize
import importlib.util
from pathlib import Path
from os.path import dirname, abspath, join
# Python 3 compatibility hack
try:
unicode('')
except NameError:
unicode = str
params = sys.argv[1:]
if (5 != len(params)):
print("use: thumbnails.py path_to_builder_directory path_to_input_files_directory path_to_output_files_directory width height")
exit(0)
mapping = {"[512x724]": "inside_1x_[512x724]",
"[1024x1448]": "inside_2x_[1024x1448]",
"[228x316]": "main_1x_[228x316]",
"[456x632]": "main_2x_[456x632]",
"[256x368]": "mobile_[256x368]",
"[792x1098]": "source_[792x1098]",
"[324x458]": "main_1x_[324x458]",
"[648x916]": "main_2x_[648x916]",
"[400x566]": "pop_up_[400x566]",
"[184x260]": "desktop[184x260]",
}
cur_path = os.getcwd()
base.configure_common_apps()
directory_x2t = params[0].replace("\\", "/")
directory_input = params[1].replace("\\", "/")
directory_output = params[2].replace("\\", "/")
th_width = params[3]
th_height = params[4]
docbuilder_path = os.path.join(directory_x2t, "docbuilder.py")
if not os.path.isfile(docbuilder_path):
print(f"ERROR: docbuilder.py not found in '{directory_x2t}'")
exit(1)
spec = importlib.util.spec_from_file_location("docbuilder", docbuilder_path)
docbuilder_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(docbuilder_mod)
CDocBuilder = docbuilder_mod.CDocBuilder
CDocBuilderValue = docbuilder_mod.CDocBuilderValue
#output_dir = directory_output + "/[" + str(th_width) + "x" + str(th_height) + "]"
#if base.is_dir(output_dir):
# base.delete_dir(output_dir)
#base.create_dir(output_dir)
input_files = []
for file in glob.glob(os.path.join(u"" + directory_input, u'*')):
input_files.append(file.replace("\\", "/"))
#print(input_files)
temp_dir = os.getcwd().replace("\\", "/") + "/temp"
if base.is_dir(temp_dir):
base.delete_dir(temp_dir)
base.create_dir(temp_dir)
directory_fonts = directory_x2t + "/sdkjs/common"
if not base.is_file(directory_fonts + "/AllFonts.js"):
base.cmd_in_dir(directory_x2t, "docbuilder", [], True)
# # True for fit, False for 100%
# isScaleSheetToPage = False
#
# json_fit_text = "0"
# if isScaleSheetToPage:
# json_fit_text = "1"
#
# #json_params += "'fitToWidth':" + json_fit_text + ",'fitToHeight':" + json_fit_text + ","
# if isScaleSheetToPage:
# json_params = "{'spreadsheetLayout':{'fitToWidth':1,'fitToHeight':1},"
# else:
# json_params = "{'spreadsheetLayout':{'fitToWidth':0,'fitToHeight':0},"
# json_params += "'documentLayout':{'drawPlaceHolders':true,'drawFormHighlight':true,'isPrint':true}}"
# json_params = json_params.replace("'", "&quot;")
json_params = "{"
json_params += "'spreadsheetLayout':{"
# True for fit, False for 100%
isScaleSheetToPage = False
json_fit_text = "0"
if isScaleSheetToPage:
json_fit_text = "1"
json_params += "'fitToWidth':" + json_fit_text + ",'fitToHeight':" + json_fit_text + ","
if True:
json_params += "'orientation':'landscape',"
page_margins = "'pageMargins':{'bottom':10,'footer':5,'header':5,'left':5,'right':5,'top':10}"
page_setup = "'pageSetup':{'orientation':1,'width':210,'height':297,'paperUnits':0,'scale':190," \
"'printArea':false,'horizontalDpi':600,'verticalDpi':600,'usePrinterDefaults':true,'fitToHeight':1,'fitToWidth':1}"
json_params += "'sheetsProps':{'0':{'headings':false,'printTitlesWidth':null,'printTitlesHeight':null," + page_margins + "," + page_setup + "}}},"
json_params += "'documentLayout':{'drawPlaceHolders':true,'drawFormHighlight':true,'isPrint':true},"
json_params += "'ignorePrintArea':'false'"
json_params += "}"
json_params = json_params.replace("'", "&quot;")
if not os.path.exists(directory_output):
os.mkdir(directory_output)
output_len = len(input_files)
output_cur = 1
for input_file in input_files:
if os.path.isdir(input_file):
next_dir_name = os.path.basename(input_file)
base.cmd("python", ["thumbnails_old.py", directory_x2t, input_file, os.path.join(directory_output, next_dir_name), th_width, th_height])
if base.is_dir(temp_dir):
base.delete_dir(temp_dir)
base.create_dir(temp_dir)
continue
print("process [" + str(output_cur) + " of " + str(output_len) + "]: " + str(input_file.encode("utf-8")))
width_page = th_width
height_page = th_height
json_params_file = json_params
if input_file.lower().endswith('.xlsx'):
temp_dir_builder = directory_output + "/temp_builder"
if base.is_dir(temp_dir_builder):
base.delete_dir(temp_dir_builder)
base.create_dir(temp_dir_builder)
builder = CDocBuilder()
builder.SetTmpFolder(temp_dir_builder)
builder.OpenFile(input_file)
context = builder.GetContext()
globalObj = context.GetGlobal()
cmd = """
(function(){
Api.getPrintAreaSize = function() {
return { Width:0, Height:0 };
};
var sheet = Api.GetSheets()[0];
var usedRange = sheet.GetUsedRange();
var maxCol = -1;
var maxRow = -1;
usedRange.ForEach(function (cell) {
if (cell.GetRowHeight() === 0 || cell.GetColumnWidth() === 0) {
return;
}
var row0 = cell.GetRow() - 1;
var col0 = cell.GetCol() - 1;
var hasContent = false;
var val = cell.GetValue();
if (val !== "" && val !== null && val !== undefined) {
hasContent = true;
}
if (!hasContent) {
var formula = cell.GetFormula();
if (typeof formula === 'string' && formula.indexOf("=") === 0) {
hasContent = true;
}
}
if (hasContent) {
if (col0 > maxCol) maxCol = col0;
if (row0 > maxRow) maxRow = row0;
}
});
if (maxRow < 0 || maxCol < 0) {
return;
}
var printRange = sheet.GetRange(
sheet.GetRangeByNumber(0, 0),
sheet.GetRangeByNumber(maxRow, maxCol)
);
Api.getPrintAreaSize = function() {
return { Width:printRange.Width, Height:printRange.Height };
};})();
"""
builder.ExecuteCommand(cmd)
api = globalObj['Api']
sizeWH = api.getPrintAreaSize()
wPrint = sizeWH.Get("Width").ToDouble()
hPrint = sizeWH.Get("Height").ToDouble()
print("CELL (printSize): " + str(wPrint) + "x" + str(hPrint))
if (wPrint > 1 and hPrint > wPrint):
tmp = width_page
width_page = height_page
height_page = tmp
json_params_file = json_params_file.replace("&quot;width&quot;:210,&quot;height&quot;:297", "&quot;width&quot;:297,&quot;height&quot;:210")
builder.CloseFile()
base.delete_dir(temp_dir_builder)
output_dir = os.path.join(directory_output,
os.path.splitext(os.path.basename(input_file))[0])
#output_dir = str(output_dir.encode("utf8"))
output_dir = abspath(unicode(output_dir))
if not os.path.exists(output_dir):
os.mkdir(output_dir)
output_dir = os.path.join(output_dir,
mapping["[" + str(th_width) + "x" + str(th_height) + "]"])
#output_dir = str(output_dir.encode("utf8"))
#output_dir = dirname(abspath(unicode(output_dir)))
output_dir = abspath(unicode(output_dir))
if not os.path.exists(output_dir):
os.mkdir(output_dir)
output_file = output_dir # os.path.join(output_dir, os.path.splitext(os.path.basename(input_file))[0])
xml_convert = u"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
xml_convert += u"<TaskQueueDataConvert>"
xml_convert += (u"<m_sFileFrom>" + input_file + u"</m_sFileFrom>")
xml_convert += (u"<m_sFileTo>" + output_file + u".zip</m_sFileTo>")
xml_convert += u"<m_nFormatTo>1029</m_nFormatTo>"
xml_convert += (u"<m_sAllFontsPath>" + directory_fonts + u"/AllFonts.js</m_sAllFontsPath>")
xml_convert += (u"<m_sFontDir>" + directory_fonts + u"</m_sFontDir>")
xml_convert += (u"<m_sJsonParams>" + json_params_file + u"</m_sJsonParams>")
xml_convert += u"<m_nDoctParams>1</m_nDoctParams>"
xml_convert += u"<m_oThumbnail>"
xml_convert += u"<first>false</first>"
if ((0 != width_page) and (0 != height_page)):
xml_convert += u"<aspect>16</aspect>"
xml_convert += (u"<width>" + str(width_page) + u"</width>")
xml_convert += (u"<height>" + str(height_page) + u"</height>")
xml_convert += u"</m_oThumbnail>"
xml_convert += u"<m_nDoctParams>1</m_nDoctParams>"
xml_convert += (u"<m_sTempDir>" + temp_dir + u"</m_sTempDir>")
xml_convert += u"</TaskQueueDataConvert>"
base.save_as_script(temp_dir + "/to.xml", [xml_convert])
base.cmd_in_dir(directory_x2t, "x2t", [temp_dir + "/to.xml"], True)
base.delete_dir(temp_dir)
base.create_dir(temp_dir)
base.extract_unicode(output_file + u".zip", output_file)
if os.path.exists(output_file + ".zip"):
try:
base.delete_file(output_file + ".zip")
except:
print("Error in deletin file: ", output_file + ".zip")
output_cur += 1
#output_file = output_file.replace("\\", "/")
#imnames = Path(output_file).glob("*.png")#glob.glob("/" + output_file.replace(":", "") + "/*.png")
imnames = [str(pp) for pp in Path(output_file).glob("*.png")]
#print(output_file + "/*.png", imnames)
#continue
if len(imnames) == 0:
base.delete_dir(output_file)
else:
width, height = imagesize.get(imnames[0])
print("WxH: ", width, height)
if width < height and False: #удалить вертикальные превью
base.delete_dir(output_file)
base.delete_dir(temp_dir)
os.chdir(cur_path)

View File

@ -1 +1 @@
9.3.0
9.3.1