Compare commits

...

178 Commits

Author SHA1 Message Date
a8ab257faf Merge branch hotfix/v9.0.4 into develop 2025-08-06 08:26:47 +00:00
a640943c60 Remove 3DPARTYLICENSE from desktop deploy 2025-08-05 18:06:08 +03:00
afb9b8b61a Merge pull request 'Fix bug 76209' (#102) from fix/bug-76209 into hotfix/v9.0.4
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/102
2025-08-05 06:52:50 +00:00
38c49f70e5 Merge pull request 'Up version to 9.0.4' (#101) from fix/version-9.0.4 into hotfix/v9.0.4
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/101
2025-08-05 06:52:16 +00:00
bdc40e1572 Fix bug 76209 2025-08-05 11:44:32 +05:00
73bf068cb0 Up version to 9.0.4 2025-08-04 16:19:16 +05:00
6a4d3bff53 Rename brotli module 2025-08-04 12:32:19 +03:00
dc3ceb6ef8 Change default plugins list 2025-08-03 21:29:06 +03:00
a0cc2123bf Update make.py 2025-07-25 15:08:51 +00:00
c71e5c5bde Update make.py 2025-07-25 06:47:39 +00:00
1f84a604d2 Update make.py 2025-07-24 20:14:14 +00:00
648dc2119e Add fonts to builder 2025-07-24 15:13:06 +03:00
4f746cb484 Fix desktop windows packages build 2025-07-24 12:40:01 +03:00
853c760cc4 Update make.py 2025-07-24 06:25:19 +00:00
2b6557f0ec [desktop] set test license type 2025-07-24 00:57:58 +03:00
053c0c2fe9 [desktop] revert test changes 2025-07-23 20:00:53 +00:00
caf79933d8 [desktop] test license's type 2025-07-23 20:45:04 +03:00
debe284664 Add Windows & Linux DesktopEditors Commercial build 2025-07-23 12:39:24 +03:00
16858aa7c2 Add new module 2025-07-20 14:51:56 +03:00
031a1119d6 Fix deploy 2025-07-17 18:17:42 +03:00
316c3cec26 fix conflict file 2025-07-17 09:16:17 +03:00
834fab5fc7 Update .github/workflows/git-operations.yml 2025-07-10 06:11:12 +00:00
d357abcfc9 Update .github/workflows/git-operations.yml 2025-07-10 06:09:09 +00:00
119b5f6d33 Merge pull request 'feature/git-operations' (#95) from feature/git-operations into master 2025-07-09 08:53:03 +00:00
8a70714eeb add gh action 2025-07-09 11:52:03 +03:00
90903009f4 add git operations 2025-07-09 11:43:49 +03:00
6f256be099 add get base url for git 2025-07-09 11:43:39 +03:00
d7eaef6503 Merge branch 'hotfix/v9.0.3'
# Conflicts:
#	scripts/sdkjs_common/jsdoc/config/plugins/cell.json
#	scripts/sdkjs_common/jsdoc/config/plugins/common.json
#	scripts/sdkjs_common/jsdoc/config/plugins/correct_doclets.js
#	scripts/sdkjs_common/jsdoc/config/plugins/events/correct_doclets.js
#	scripts/sdkjs_common/jsdoc/config/plugins/forms.json
#	scripts/sdkjs_common/jsdoc/config/plugins/methods/cell.json
#	scripts/sdkjs_common/jsdoc/config/plugins/methods/common.json
#	scripts/sdkjs_common/jsdoc/config/plugins/methods/forms.json
#	scripts/sdkjs_common/jsdoc/config/plugins/methods/slide.json
#	scripts/sdkjs_common/jsdoc/config/plugins/methods/word.json
#	scripts/sdkjs_common/jsdoc/config/plugins/slide.json
#	scripts/sdkjs_common/jsdoc/config/plugins/word.json
#	scripts/sdkjs_common/jsdoc/office-api/config/cell.json
#	scripts/sdkjs_common/jsdoc/office-api/config/forms.json
#	scripts/sdkjs_common/jsdoc/office-api/config/pdf.json
#	scripts/sdkjs_common/jsdoc/office-api/config/slide.json
#	scripts/sdkjs_common/jsdoc/office-api/config/word.json
#	scripts/sdkjs_common/jsdoc/office-api/generate_docs_md.py
#	scripts/sdkjs_common/jsdoc/plugins/config/correct_doclets.js
#	scripts/sdkjs_common/jsdoc/plugins/generate_docs_events_json.py
#	version
2025-07-08 17:59:48 +03:00
0c7b8a2b1c Merge pull request '[jsdoc] Fix replace <br> tag' (#93) from fix/js-doc into hotfix/v9.0.3
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/93
2025-07-08 08:57:03 +00:00
9b5b3eb77c [jsdoc] Fix replace <br> tag 2025-07-08 15:51:43 +07:00
9e31770bfa Merge pull request 'fix/js-doc' (#92) from fix/js-doc into hotfix/v9.0.3
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/92
2025-07-08 08:17:40 +00:00
4d6b9f9463 Fix md 2025-07-08 15:15:55 +07:00
d2c79bb78d [jsdoc] Refactoring 2025-07-08 15:15:49 +07:00
bfd3bb009f Merge branch hotfix/v9.0.2 into develop 2025-07-07 14:37:18 +00:00
7ef302fac1 Merge branch hotfix/v9.0.2 into master 2025-07-07 14:37:18 +00:00
fece05de0b Merge pull request 'Up version to 9.0.3' (#90) from fix/version9.0.3 into hotfix/v9.0.3
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/90
2025-07-07 08:44:32 +00:00
71a2981ae8 Up version to 9.0.3 2025-07-07 13:12:43 +05:00
de1d437576 Remove temporary logs. Add verbose to install modules if failed 2025-07-03 13:10:37 +03:00
2e179644b3 Add verbose for debug npm/grunt 2025-07-03 11:50:44 +03:00
c4551af253 Add temporary logs 2025-07-03 11:07:08 +03:00
31f679a050 [js] added some logs 2025-06-30 17:10:30 +03:00
ea52e70a6d Merge pull request '[macros][libs] Remove extra methods info' (#88) from fix/macros-libs into hotfix/v9.0.3
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/88
2025-06-30 11:34:32 +00:00
1d721e3e3e [macros][libs] Remove extra methods info 2025-06-30 18:33:47 +07:00
3e3b0127a6 Merge pull request '[macros][libs] Added forms to generation' (#87) from fix/macros-libs into hotfix/v9.0.3
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/87
2025-06-30 11:02:31 +00:00
dcc9f8e669 [macros][libs] Added forms to generation 2025-06-30 17:58:52 +07:00
c3dce4bc91 Merge branch release/v9.0.0 into develop 2025-06-27 14:17:01 +00:00
990382512b Merge branch release/v9.0.0 into master 2025-06-27 14:17:01 +00:00
b6985ce27e Merge pull request '[jsdoc] Generating plugin events docs' (#86) from feature/jsdoc-plugin-events into hotfix/v9.0.2
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/86
2025-06-27 08:39:47 +00:00
65e36cd01a [jsdoc] Generating plugin events docs 2025-06-27 15:35:17 +07:00
caaebde240 [develop] Add note to clarify that ARM is unsupported 2025-06-27 10:37:36 +03:00
7c130faac2 Merge branch hotfix/v9.0.2 into master 2025-06-26 12:53:46 +00:00
7ff5d2f40d Merge pull request 'Up version to 9.0.2' (#85) from fix/version9.0.2 into hotfix/v9.0.2
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/85
2025-06-26 07:46:05 +00:00
a353e89871 Up version to 9.0.2 2025-06-26 12:40:26 +05:00
03f99c526d Merge pull request 'Add built-in Date to primitive types' (#84) from fix/generation-data-type into release/v9.0.0 2025-06-19 13:49:23 +00:00
34a54bf88f Add built-in Date to primitive types 2025-06-19 16:42:20 +03:00
b40c0a0d74 Merge branch release/v9.0.0 into master 2025-06-18 09:22:00 +00:00
e4cc090bfd Merge pull request '[build] Bump Ubuntu to 20.04(Dockerfile); switch to @yao-pkg/pkg; For bug 74773' (#83) from fix/bug-74773-2 into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/83
2025-06-11 12:56:35 +00:00
c6138b3902 Merge pull request 'Up version to 9.0.0' (#82) from fix/version9.0.0 into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/82
2025-06-10 07:22:28 +00:00
8ec240dcee Up version to 9.0.0 2025-06-10 11:34:31 +05:00
fa589c9523 [build] Bump Ubuntu to 20.04(Dockerfile); switch to @yao-pkg/pkg; For bug 74773 2025-06-06 17:45:07 +03:00
829228d28c Fix builder deploy s3 paths 2025-06-05 16:49:13 +03:00
a7c9f3a0ce Add builder python wheel package build 2025-06-05 12:09:40 +03:00
e676ebcffd Enable minimization for native editors 2025-05-29 23:12:26 +03:00
6fe22b14c6 Add script for update dictionaries 2025-05-29 22:34:42 +03:00
d50b171b54 Optimize packages build 2025-05-28 13:24:18 +03:00
19fc33b7f5 [build] Bump nodejs version to 20 for all environments 2025-05-15 11:55:53 +03:00
d6cbfcbfe3 [build] Bump nodejs version to 20 for all environments 2025-05-15 11:46:05 +03:00
23db442c82 [build] Downgrade to nodejs packaging version to 20 2025-05-10 16:14:27 +03:00
e40f8c9a7e [build] Downgrade to nodejs packaging version to 20 2025-05-10 16:13:42 +03:00
4d4d1612ce [build] Bump nodejs version to 22(linux only) 2025-05-08 01:22:34 +03:00
ded3dfa63c [build] Bump nodejs version to 22 2025-05-08 01:22:33 +03:00
6a2db3d59e [build] Bump nodejs version to 22(linux only) 2025-05-08 01:21:26 +03:00
48c8a635a8 [build] Bump nodejs version to 22 2025-05-08 01:11:23 +03:00
1ac83e0ffd Merge pull request '[jsdoc] Escape [] in quotes' (#78) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/78
2025-05-07 08:42:37 +00:00
ef6ecbbebd [jsdoc] Escape [] in quotes 2025-05-07 15:38:56 +07:00
f51b841320 Merge pull request 'Added md fetch' (#77) from feature/md2html into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/77
2025-05-05 19:09:39 +00:00
7c0099c57e Added md fetch 2025-05-05 15:21:00 +03:00
8f49dce1ed Merge pull request 'refactor: remove semicolon from python scripts' (#76) from pysemicolon into release/v9.0.0 2025-05-01 05:33:19 +00:00
a9bad0d5b7 Fix check arm system 2025-04-23 14:35:34 +03:00
702a83c010 Merge pull request 'Added OFD format' (#73) from feature/ofd into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/73
2025-04-23 09:31:37 +00:00
227ecbde99 [deploy] Add check is_exist for addon path 2025-04-22 16:08:42 +03:00
ee52dbe5c4 [deploy] Add check is_exist for addon path 2025-04-22 15:41:28 +03:00
0f0e0a0e52 [deploy] Build and deploy server grunt module 2025-04-22 10:40:30 +03:00
d288d6326c Merge branch hotfix/v8.3.3 into develop 2025-04-21 08:57:49 +00:00
b87e305c06 Merge branch hotfix/v8.3.3 into release/v9.0.0 2025-04-21 08:57:49 +00:00
8516b163b4 Added OFD format 2025-04-19 17:35:55 +03:00
4460d6ed13 Merge branch hotfix/v8.3.3 into master 2025-04-16 07:30:59 +00:00
ab3165fdaf Add script for minimization js 2025-04-14 20:57:52 +03:00
1539c187e3 Merge pull request 'Up version to 8.3.3' (#72) from fix/version8.3.3 into hotfix/v8.3.3
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/72
2025-04-14 10:11:35 +00:00
78df8eb494 Up version to 8.3.3 2025-04-14 14:52:43 +05:00
eebbd513d3 Added Python script for creating and launching the Libreoffice build 2025-04-08 09:47:48 +00:00
2ab7616132 Fix typo 2025-04-07 12:25:42 +03:00
3be471b472 Merge pull request 'feature/clang' (#71) from feature/clang into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/71
2025-04-06 09:55:15 +00:00
a151375339 Remove spaces 2025-04-06 12:54:23 +03:00
ed2ab2d80b Refactoring 2025-04-06 12:53:03 +03:00
35f99ac3a0 Fix icu build 2025-04-04 10:20:24 +03:00
836a0401ed Testing clang build 2025-04-04 08:55:38 +03:00
b8024df7d3 Merge branch hotfix/v8.3.2 into release/v9.0.0 2025-04-01 08:32:43 +00:00
74c02f9d50 Merge branch hotfix/v8.3.2 into hotfix/v8.3.3 2025-04-01 08:32:42 +00:00
25a1e16824 Merge branch hotfix/v8.3.2 into develop 2025-04-01 08:32:41 +00:00
7ee66bbafd Merge branch hotfix/v8.3.2 into master 2025-04-01 08:32:40 +00:00
2b07d1aa4d Fix build 2025-04-01 10:50:23 +03:00
c6acd6cdcd Merge pull request '[jsdoc] Fix write inherited methods' (#69) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/69
2025-03-27 11:42:25 +00:00
cee122afa5 [jsdoc] Fix write inherited methods 2025-03-27 09:53:10 +07:00
4c76406f8c Fix folder creation 2025-03-26 15:49:50 +03:00
6fd89057ec Fix typo 2025-03-26 15:41:48 +03:00
ae8b77628e Fix build 2025-03-26 15:31:21 +03:00
959d919d9e Merge pull request 'hotfix/v8.3.2' (#68) from hotfix/v8.3.2 into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/68
2025-03-21 11:44:01 +00:00
bf7df0b45a Merge pull request 'hotfix/v8.3.2' (#67) from hotfix/v8.3.2 into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/67
2025-03-21 11:43:22 +00:00
3589ea0f60 Fix bug with duplicate projects 2025-03-21 10:24:44 +03:00
ad23ee2803 Merge branch hotfix/v8.3.2 into master 2025-03-19 12:43:41 +00:00
bc59f739f5 Merge pull request 'Up version to 8.3.2' (#66) from fix/version8.3.2 into hotfix/v8.3.2
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/66
2025-03-18 11:09:00 +00:00
b8e42184f8 Up version to 8.3.2 2025-03-18 15:54:05 +05:00
50c312513c Merge pull request '[jsdoc] Fix dataset system message' (#65) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/65
2025-03-18 10:01:27 +00:00
427ae97dd2 [jsdoc] Fix dataset system message 2025-03-18 16:59:58 +07:00
d80f1f1b0f [develop] Parse config to fix install_mysqlserver 2025-03-18 10:19:18 +03:00
520d779f04 [develop] Add information log message to restart_win_rabbit 2025-03-17 10:55:53 +03:00
c4b938b7db [deploy] Build and deploy server grunt module 2025-03-16 18:23:22 +03:00
9435cdc99b Merge pull request 'Added html 3dParty version control' (#64) from fix/html into hotfix/v8.3.2
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/64
2025-03-12 16:54:41 +00:00
cf90a5ce21 Added html 3dParty version control 2025-03-12 19:50:03 +03:00
3f4d0cefa8 Merge pull request 'hotfix/v8.3.2' (#63) from hotfix/v8.3.2 into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/63
2025-03-12 07:58:39 +00:00
12ce537781 [develop] Parse config to fix install_mysqlserver 2025-03-10 20:52:36 +03:00
12d824fe2d Merge branch hotfix/v8.3.1 into develop 2025-03-10 08:25:10 +00:00
8dcf0277ac Merge pull request '[jsdoc] Fix typo' (#62) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/62
2025-03-10 07:49:10 +00:00
6664051127 [jsdoc] Fix typo 2025-03-10 14:47:55 +07:00
019f10ee86 Merge pull request '[jsdoc] Fix expression example' (#61) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/61
2025-03-07 14:59:01 +00:00
a7ee5d5679 [jsdoc] Generation for site 2025-03-07 21:36:25 +07:00
7c31890fc0 [jsdoc] Fix expression example 2025-03-07 16:41:06 +07:00
ad9258710b Merge pull request '[jsdoc] Fix prev' (#60) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/60
2025-03-07 09:21:06 +00:00
626dd37312 [jsdoc] Fix prev 2025-03-07 16:07:23 +07:00
b4d95ccbb9 Merge pull request '[jsdoc] Rename md docs folders' (#58) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/58
2025-03-07 08:54:22 +00:00
0d0ae2b5e6 [jsdoc] Rename md docs folders 2025-03-07 15:49:57 +07:00
d7b3b7f120 Merge pull request '[jsdoc] Write only used enums' (#57) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/57
2025-03-07 08:20:20 +00:00
7a864171b3 [jsdoc] Write only used enums 2025-03-07 15:04:48 +07:00
8cfb8f3d84 Merge pull request '[jsdoc] Fix path' (#56) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/56
2025-03-06 13:58:01 +00:00
2f39454d31 [jsdoc] Fix path 2025-03-06 20:51:42 +07:00
de33755900 Remove unused files 2025-03-06 15:06:49 +03:00
d3d1080c89 Merge pull request '[jsdoc] Fix bug, sort props/methods' (#55) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/55
2025-03-06 11:35:59 +00:00
539597f07c [jsdoc] Fix bug, sort props/methods, handle @link tag 2025-03-06 18:30:24 +07:00
6e87116634 Fix typo 2025-03-06 13:56:55 +03:00
029b16ca68 Fix deploy builder 2025-03-06 13:05:25 +03:00
40b11e192d Merge pull request '[jsdoc] Removed editor tag in plugins examples' (#54) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/54
2025-03-06 06:45:33 +00:00
72cc19f346 [jsdoc] Removed editor tag in plugins examples 2025-03-06 13:43:56 +07:00
efb22f741f Merge pull request '[jsdoc] Fixes docs generation' (#53) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/53
2025-03-05 12:07:32 +00:00
2458673d3c [jsdoc] Fixes docs generation 2025-03-05 19:01:28 +07:00
3881a6659e Merge pull request '[jsdoc] Class markdown, data types fixes' (#52) from fix/js-doc into release/v9.0.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/52
2025-03-04 12:59:06 +00:00
a567cc2222 [jsdoc] Class markdown, data types fixes 2025-03-04 19:43:48 +07:00
6a9b2bac4a Merge pull request 'feature/build-lo' (#51) from feature/build-lo into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/51
2025-03-04 08:42:49 +00:00
87542f4a56 [license_checker] Update config 2025-02-28 12:17:58 +03:00
32b47cd21e Merge branch hotfix/v8.3.1 into master 2025-02-27 13:16:17 +00:00
bf75e1c062 Merge pull request 'Up version to 8.3.1' (#50) from fix/version8.3.1 into hotfix/v8.3.1
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/50
2025-02-27 13:15:05 +00:00
97fccfa34b Up version to 8.3.1 2025-02-27 12:07:55 +05:00
1ed32fe71c [build] added disable_sln flag 2025-02-26 10:57:24 +03:00
3ce8f251a1 [build] fix build_lo_windows.py added build_vs_integration 2025-02-26 10:40:03 +03:00
e2ad38f297 [develop] Add "vs-ide-integration" make param 2025-02-25 21:48:52 +03:00
993303bfa4 [develop] Add "lo_build_path" param; Remove "requests" import; Fix LO build 2025-02-24 23:09:54 +03:00
50d9460f63 [build] fix build_lo_windows.py 2025-02-24 12:28:42 +03:00
4b02b57c07 [build] added script for building LO on Windows 2025-02-21 06:56:57 +03:00
1fc9382ce9 Merge pull request 'hotfix/v8.3.1' (#49) from hotfix/v8.3.1 into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/49
2025-02-20 13:31:20 +00:00
ea43e67fe8 Merge pull request '[jsdoc] Added editor name to examples' (#48) from fix/js-doc into hotfix/v8.3.1
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/48
2025-02-19 17:34:37 +00:00
dd28a41e17 [jsdoc] Added editor name to examples 2025-02-20 00:30:45 +07:00
b11a273d65 Merge pull request '[jsdoc] Remove model field if empty' (#46) from fix/js-doc into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/46
2025-02-12 05:45:02 +00:00
d4ee25b004 [jsdoc] Remove model field if empty 2025-02-12 12:35:28 +07:00
a2b7719100 [v8] Disable git configuration on windows 2025-02-11 21:32:05 +03:00
1e6cde4d98 Merge pull request '[jsdoc] Added generation dataset script' (#45) from feauture/dataset-script into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/45
2025-02-11 08:05:25 +00:00
34f627d146 [jsdoc] Added generation dataset script 2025-02-11 15:01:58 +07:00
54accd4394 Merge pull request '[jsdoc] Escape "|" in tables md' (#44) from fix/js-doc into hotfix/v8.3.1
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/44
2025-02-11 07:56:03 +00:00
63557fba56 [jsdoc] Escape "|" in tables md 2025-02-11 14:54:14 +07:00
7a4be158c2 Merge pull request 'Online installer: switch to MSVC2015' (#41) from feature/online-installer-msvc2015 into develop 2025-02-06 15:30:10 +00:00
810e12bd22 Merge release/8.3.0 into master 2025-02-06 07:41:46 +00:00
066f7ad8c1 Version Up 2025-02-06 10:31:28 +03:00
e52a654731 Merge branch release/v8.3.0 into master 2025-02-05 17:13:36 +00:00
170a511654 [win] online installer: switch to MSVC2015 2025-01-30 13:24:51 +02:00
11c783f088 Merge pull request 'release/v8.3.0' (#38) from release/v8.3.0 into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/38
2025-01-26 07:48:28 +00:00
053e317850 Merge pull request 'release/v8.3.0' (#29) from release/v8.3.0 into develop
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/build_tools/pulls/29
2025-01-02 19:54:05 +00:00
d6096431bd Merge branch hotfix/v8.2.2 into develop 2024-12-09 11:57:11 +00:00
a60bc78e23 Merge branch hotfix/v8.2.2 into master 2024-11-28 12:13:38 +00:00
adc353cdcc refactor: remove semicolon from python scripts
Semicolons are unnecessary when there's only one statement on a single line.
2024-09-29 21:46:41 +08:00
76 changed files with 3670 additions and 780 deletions

88
.github/workflows/git-operations.yml vendored Normal file
View File

@ -0,0 +1,88 @@
name: Git Operations
on:
workflow_dispatch:
inputs:
operation:
description: 'Operation to perform'
required: true
type: choice
options:
- create
- remove
default: 'create'
branch_name:
description: 'Branch name to create or remove'
required: true
type: string
base_branch:
description: 'Base branch to work from (for create operation)'
required: false
type: string
default: 'develop'
branding:
description: 'Branding name'
required: false
type: string
default: 'onlyoffice'
branding_url:
description: 'Branding repository URL (relative to git host)'
required: false
type: string
default: 'ONLYOFFICE/onlyoffice.git'
jobs:
git-operations:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
path: ONLYOFFICE/build_tools
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# Install any Python dependencies if requirements.txt exists
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Configure Git
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions@github.com"
- name: Run Git Operations
run: |
cd ONLYOFFICE/build_tools/scripts/develop
python git_operations.py ${{ inputs.operation }} "${{ inputs.branch_name }}" \
--base-branch="${{ inputs.base_branch }}" \
--branding="${{ inputs.branding }}" \
--branding-url="${{ inputs.branding_url }}" \
--modules="${{ inputs.modules }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Operation Summary
run: |
echo "## Git Operations Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Operation**: ${{ inputs.operation }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch Name**: ${{ inputs.branch_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Base Branch**: ${{ inputs.base_branch }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branding**: ${{ inputs.branding }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branding URL**: ${{ inputs.branding_url }}" >> $GITHUB_STEP_SUMMARY
echo "- **Modules**: ${{ inputs.modules }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ inputs.operation }}" = "remove" ] && [ "${{ inputs.force_remove }}" = "true" ]; then
echo "- **Force Remove**: Yes" >> $GITHUB_STEP_SUMMARY
fi

View File

@ -1,14 +1,20 @@
FROM ubuntu:16.04
FROM ubuntu:20.04
ENV TZ=Etc/UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get -y update && \
apt-get -y install python \
python3 \
apt-get -y install tar \
sudo
RUN rm /usr/bin/python && ln -s /usr/bin/python2 /usr/bin/python
ADD . /build_tools
WORKDIR /build_tools
RUN mkdir -p /opt/python3 && \
tar -xzf /build_tools/tools/linux/python3.tar.gz -C /opt/python3 --strip-components=1
ENV PATH="/opt/python3/bin:${PATH}"
RUN ln -s /opt/python3/bin/python3.10 /usr/bin/python
CMD ["sh", "-c", "cd tools/linux && python3 ./automate.py"]

View File

@ -8,8 +8,8 @@ necessary for the compilation process, all the dependencies required for the
correct work, as well as to get the latest version of
**ONLYOFFICE products** source code and build all their components.
**Important!** We can only guarantee the correct work of the products built from
the `master` branch.
**Important!** We can only guarantee the correct work of the products built
from the `master` branch.
## How to use - Linux

View File

@ -1,3 +1,3 @@
sdkjs-plugin="photoeditor, macros, ocr, translator, thesaurus, youtube, highlightcode, zotero"
sdkjs-plugin-server="speech, zotero, mendeley, speechrecognition, drawio"
sdkjs-plugin="ai, photoeditor, ocr, translator, thesaurus, youtube, highlightcode"
sdkjs-plugin-server="speech, zotero, mendeley, speechrecognition"
sdkjs-addons="sdkjs-forms"

View File

@ -6,6 +6,10 @@ but don't want to compile pretty compilcated 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

View File

@ -8,7 +8,7 @@ import run_server
import config
import base
git_dir = sys.argv[1];
git_dir = sys.argv[1]
base.print_info('argv :'+' '.join(sys.argv))
base.cmd_in_dir(git_dir + '/build_tools/', 'python3', ['configure.py', '--develop', '1'] + sys.argv[2:])
@ -18,7 +18,7 @@ config.parse_defaults()
if base.is_exist(git_dir + "/server/FileConverter/bin/fonts.log"):
base.print_info('remove font cache to regenerate fonts in external sdkjs volume')
base.delete_file(git_dir + "/server/FileConverter/bin/fonts.log");
base.delete_file(git_dir + "/server/FileConverter/bin/fonts.log")
# external server volume
if base.is_exist(sys.argv[1] + '/server/DocService/package.json'):
@ -28,7 +28,7 @@ if base.is_exist(sys.argv[1] + '/server/DocService/package.json'):
base.replaceInFileRE("/etc/supervisor/conf.d/ds-converter.conf", "command=.*", "command=node " + git_dir + "/server/FileConverter/sources/convertermaster.js")
base.replaceInFileRE("/app/ds/setup/config/supervisor/ds/ds-converter.conf", "command=.*", "command=node " + git_dir + "/server/FileConverter/sources/convertermaster.js")
base.print_info('run_server.run_docker_server')
run_server.run_docker_server();
run_server.run_docker_server()
else:
#Fix theme generation for external sdkjs volume
if base.is_exist(git_dir + "/server/FileConverter/bin/DoctRenderer.config"):

View File

@ -428,6 +428,13 @@ def cmd_in_dir(directory, prog, args=[], is_no_errors=False):
os.chdir(cur_dir)
return ret
def cmd_in_dir_qemu(platform, directory, prog, args=[], is_no_errors=False):
if (platform == "linux_arm64"):
return cmd_in_dir(directory, "qemu-aarch64", ["-L", "/usr/aarch64-linux-gnu", prog] + args, is_no_errors)
if (platform == "linux_arm32"):
return cmd_in_dir(directory, "qemu-arm", ["-L", "/usr/arm-linux-gnueabi", prog] + args, is_no_errors)
return 0
def cmd_and_return_cwd(prog, args=[], is_no_errors=False):
cur_dir = os.getcwd()
ret = cmd(prog, args, is_no_errors)
@ -514,6 +521,27 @@ def git_get_origin():
os.chdir(cur_dir)
return ret
def git_get_base_url():
"""Get the base URL for git operations, with fallback to GitHub"""
origin = git_get_origin()
if origin:
# Extract base URL from origin
if origin.startswith("https://"):
# For HTTPS URLs like https://git.example.com/owner/repo.git
parts = origin.split("/")
if len(parts) >= 4:
return "/".join(parts[:3]) + "/"
elif ":" in origin and "@" in origin:
# For SSH URLs like git@git.example.com:owner/repo.git
at_pos = origin.find("@")
colon_pos = origin.find(":", at_pos)
if at_pos != -1 and colon_pos != -1:
host = origin[at_pos+1:colon_pos]
return f"https://{host}/"
# Fallback to GitHub
return "https://github.com/"
def git_is_ssh():
git_protocol = config.option("git-protocol")
if (git_protocol == "https"):
@ -535,7 +563,7 @@ def get_ssh_base_url():
def git_update(repo, is_no_errors=False, is_current_dir=False, git_owner=""):
print("[git] update: " + repo)
owner = git_owner if git_owner else "ONLYOFFICE"
url = "https://github.com/" + owner + "/" + repo + ".git"
url = git_get_base_url() + owner + "/" + repo + ".git"
if git_is_ssh():
url = get_ssh_base_url() + repo + ".git"
folder = get_script_dir() + "/../../" + repo
@ -607,7 +635,7 @@ def get_branding_repositories(checker):
def create_pull_request(branches_to, repo, is_no_errors=False, is_current_dir=False):
print("[git] create pull request: " + repo)
url = "https://github.com/ONLYOFFICE/" + repo + ".git"
url = git_get_base_url() + "ONLYOFFICE/" + repo + ".git"
if git_is_ssh():
url = get_ssh_base_url() + repo + ".git"
folder = get_script_dir() + "/../../" + repo
@ -646,7 +674,7 @@ def update_repositories(repositories):
git_update(repo, value[0], False)
else:
if is_dir(current_dir + "/.git"):
delete_dir_with_access_error(current_dir);
delete_dir_with_access_error(current_dir)
delete_dir(current_dir)
if not is_dir(current_dir):
create_dir(current_dir)
@ -1226,19 +1254,20 @@ def mac_correct_rpath_x2t(dir):
mac_correct_rpath_library("kernel", ["UnicodeConverter"])
mac_correct_rpath_library("kernel_network", ["UnicodeConverter", "kernel"])
mac_correct_rpath_library("graphics", ["UnicodeConverter", "kernel"])
mac_correct_rpath_library("doctrenderer", ["UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "DjVuFile", "DocxRenderer"])
mac_correct_rpath_library("doctrenderer", ["UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "DocxRenderer"])
mac_correct_rpath_library("HtmlFile2", ["UnicodeConverter", "kernel", "kernel_network", "graphics"])
mac_correct_rpath_library("EpubFile", ["UnicodeConverter", "kernel", "HtmlFile2", "graphics"])
mac_correct_rpath_library("Fb2File", ["UnicodeConverter", "kernel", "graphics"])
mac_correct_rpath_library("PdfFile", ["UnicodeConverter", "kernel", "graphics", "kernel_network"])
mac_correct_rpath_library("DjVuFile", ["UnicodeConverter", "kernel", "graphics", "PdfFile"])
mac_correct_rpath_library("XpsFile", ["UnicodeConverter", "kernel", "graphics", "PdfFile"])
mac_correct_rpath_library("OFDFile", ["UnicodeConverter", "kernel", "graphics", "PdfFile"])
mac_correct_rpath_library("DocxRenderer", ["UnicodeConverter", "kernel", "graphics"])
mac_correct_rpath_library("IWorkFile", ["UnicodeConverter", "kernel"])
mac_correct_rpath_library("HWPFile", ["UnicodeConverter", "kernel", "graphics"])
cmd("chmod", ["-v", "+x", "./x2t"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./x2t"], True)
mac_correct_rpath_binary("./x2t", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "DjVuFile", "HtmlFile2", "Fb2File", "EpubFile", "doctrenderer", "DocxRenderer", "IWorkFile", "HWPFile"])
mac_correct_rpath_binary("./x2t", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "HtmlFile2", "Fb2File", "EpubFile", "doctrenderer", "DocxRenderer", "IWorkFile", "HWPFile"])
if is_file("./allfontsgen"):
cmd("chmod", ["-v", "+x", "./allfontsgen"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./allfontsgen"], True)
@ -1246,7 +1275,7 @@ def mac_correct_rpath_x2t(dir):
if is_file("./allthemesgen"):
cmd("chmod", ["-v", "+x", "./allthemesgen"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./allthemesgen"], True)
mac_correct_rpath_binary("./allthemesgen", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "graphics", "kernel_network", "doctrenderer", "PdfFile", "XpsFile", "DjVuFile", "DocxRenderer"])
mac_correct_rpath_binary("./allthemesgen", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "graphics", "kernel_network", "doctrenderer", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "DocxRenderer"])
if is_file("./pluginsmanager"):
cmd("chmod", ["-v", "+x", "./pluginsmanager"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./pluginsmanager"], True)
@ -1263,14 +1292,14 @@ def mac_correct_rpath_docbuilder(dir):
os.chdir(dir)
cmd("chmod", ["-v", "+x", "./docbuilder"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./docbuilder"], True)
mac_correct_rpath_binary("./docbuilder", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "DjVuFile", "HtmlFile2", "Fb2File", "EpubFile", "IWorkFile", "HWPFile", "doctrenderer", "DocxRenderer"])
mac_correct_rpath_library("docbuilder.c", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "doctrenderer", "PdfFile", "XpsFile", "DjVuFile", "DocxRenderer"])
mac_correct_rpath_binary("./docbuilder", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "HtmlFile2", "Fb2File", "EpubFile", "IWorkFile", "HWPFile", "doctrenderer", "DocxRenderer"])
mac_correct_rpath_library("docbuilder.c", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "doctrenderer", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "DocxRenderer"])
def add_loader_path_to_rpath(libs):
for lib in libs:
cmd("install_name_tool", ["-add_rpath", "@loader_path", "lib" + lib + ".dylib"], True)
add_loader_path_to_rpath(["icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "doctrenderer", "PdfFile", "XpsFile", "DjVuFile", "DocxRenderer", "docbuilder.c"])
add_loader_path_to_rpath(["icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "doctrenderer", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "DocxRenderer", "docbuilder.c"])
os.chdir(cur_dir)
return
@ -1280,9 +1309,9 @@ def mac_correct_rpath_desktop(dir):
os.chdir(dir)
mac_correct_rpath_library("hunspell", [])
mac_correct_rpath_library("ooxmlsignature", ["kernel"])
mac_correct_rpath_library("ascdocumentscore", ["UnicodeConverter", "kernel", "graphics", "kernel_network", "PdfFile", "XpsFile", "DjVuFile", "hunspell", "ooxmlsignature"])
mac_correct_rpath_library("ascdocumentscore", ["UnicodeConverter", "kernel", "graphics", "kernel_network", "PdfFile", "XpsFile", "DjVuFile", "hunspell", "ooxmlsignature", "doctrenderer"])
cmd("install_name_tool", ["-change", "@executable_path/../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework", "@rpath/Chromium Embedded Framework.framework/Chromium Embedded Framework", "libascdocumentscore.dylib"])
mac_correct_rpath_binary("./editors_helper.app/Contents/MacOS/editors_helper", ["ascdocumentscore", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "DjVuFile", "hunspell", "ooxmlsignature"])
mac_correct_rpath_binary("./editors_helper.app/Contents/MacOS/editors_helper", ["ascdocumentscore", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "XpsFile", "OFDFile", "DjVuFile", "hunspell", "ooxmlsignature", "doctrenderer"])
cmd("install_name_tool", ["-add_rpath", "@executable_path/../../../../Frameworks", "./editors_helper.app/Contents/MacOS/editors_helper"], True)
cmd("install_name_tool", ["-add_rpath", "@executable_path/../../../../Resources/converter", "./editors_helper.app/Contents/MacOS/editors_helper"], True)
cmd("chmod", ["-v", "+x", "./editors_helper.app/Contents/MacOS/editors_helper"])
@ -1316,7 +1345,7 @@ def linux_set_origin_rpath_libraries(dir, libs):
return
def linux_correct_rpath_docbuilder(dir):
linux_set_origin_rpath_libraries(dir, ["docbuilder.jni.so", "docbuilder.c.so", "icuuc.so.58", "doctrenderer.so", "graphics.so", "kernel.so", "kernel_network.so", "UnicodeConverter.so", "PdfFile.so", "XpsFile.so", "DjVuFile.so", "DocxRenderer.so"])
linux_set_origin_rpath_libraries(dir, ["docbuilder.jni.so", "docbuilder.c.so", "icuuc.so.58", "doctrenderer.so", "graphics.so", "kernel.so", "kernel_network.so", "UnicodeConverter.so", "PdfFile.so", "XpsFile.so", "OFDFile.so", "DjVuFile.so", "DocxRenderer.so"])
return
def common_check_version(name, good_version, clean_func):
@ -1399,7 +1428,7 @@ def copy_sdkjs_plugins(dst_dir, is_name_as_guid=False, is_desktop_local=False, i
plugins_dir = __file__script__path__ + "/../../onlyoffice.github.io/sdkjs-plugins/content"
plugins_list_config = config.option("sdkjs-plugin")
if isXp:
plugins_list_config="photoeditor, macros, highlightcode, doc2md"
plugins_list_config="photoeditor, highlightcode, doc2md"
if ("" == plugins_list_config):
return
plugins_list = plugins_list_config.rsplit(", ")
@ -1613,7 +1642,7 @@ def restorePathForBuilder(new_path):
old_path = new_path[:-4]
delete_file(old_path)
copy_file(new_path, old_path)
delete_file(new_path);
delete_file(new_path)
return
def generate_check_linux_system(build_tools_dir, out_dir):
@ -1805,8 +1834,10 @@ def get_autobuild_version(product, platform="", branch="", build=""):
download_platform = platform
if ("" == download_platform):
osType = get_platform()
isArm = True if (-1 != osType.find("arm")) else False
is64 = True if (osType.endswith("64")) else False
isArm = False
if (-1 != osType.find("arm")) or (-1 != osType.find("aarch64")):
isArm = True
if ("windows" == host_platform()):
download_platform = "win-"
@ -1831,10 +1862,14 @@ def get_autobuild_version(product, platform="", branch="", build=""):
download_addon = download_branch + "/" + download_build + "/" + product + "-" + download_platform + ".7z"
return "http://repo-doc-onlyoffice-com.s3.amazonaws.com/archive/" + download_addon
def create_x2t_js_cache(dir, product):
def create_x2t_js_cache(dir, product, platform):
if is_file(dir + "/libdoctrenderer.dylib") and (os.path.getsize(dir + "/libdoctrenderer.dylib") < 5*1024*1024):
return
if ((platform == "linux_arm64") and not is_os_arm()):
cmd_in_dir_qemu(platform, dir, "./x2t", ["-create-js-snapshots"], True)
return
cmd_in_dir(dir, "./x2t", ["-create-js-snapshots"], True)
return
@ -1842,4 +1877,3 @@ def setup_local_qmake(dir_qmake):
dir_base = os.path.dirname(dir_qmake)
writeFile(dir_base + "/onlyoffice_qt.conf", "Prefix = " + dir_base)
return

View File

@ -33,24 +33,29 @@ def make():
base.set_env('NODE_ENV', 'production')
base_dir = base.get_script_dir() + "/.."
out_dir = base_dir + "/out/js/";
out_dir = base_dir + "/out/js/"
branding = config.option("branding-name")
if ("" == branding):
branding = "onlyoffice"
out_dir += branding
base.create_dir(out_dir)
isOnlyMobile = False
if (config.option("module") == "mobile"):
isOnlyMobile = True
# builder
base.cmd_in_dir(base_dir + "/../web-apps/translation", "python", ["merge_and_check.py"])
build_interface(base_dir + "/../web-apps/build")
build_sdk_builder(base_dir + "/../sdkjs/build")
base.create_dir(out_dir + "/builder")
base.copy_dir(base_dir + "/../web-apps/deploy/web-apps", out_dir + "/builder/web-apps")
base.copy_dir(base_dir + "/../sdkjs/deploy/sdkjs", out_dir + "/builder/sdkjs")
correct_sdkjs_licence(out_dir + "/builder/sdkjs")
if not isOnlyMobile:
base.cmd_in_dir(base_dir + "/../web-apps/translation", "python", ["merge_and_check.py"])
build_interface(base_dir + "/../web-apps/build")
build_sdk_builder(base_dir + "/../sdkjs/build")
base.create_dir(out_dir + "/builder")
base.copy_dir(base_dir + "/../web-apps/deploy/web-apps", out_dir + "/builder/web-apps")
base.copy_dir(base_dir + "/../sdkjs/deploy/sdkjs", out_dir + "/builder/sdkjs")
correct_sdkjs_licence(out_dir + "/builder/sdkjs")
# desktop
if config.check_option("module", "desktop"):
if config.check_option("module", "desktop") and not isOnlyMobile:
build_sdk_desktop(base_dir + "/../sdkjs/build")
base.create_dir(out_dir + "/desktop")
base.copy_dir(base_dir + "/../sdkjs/deploy/sdkjs", out_dir + "/desktop/sdkjs")
@ -72,7 +77,7 @@ def make():
# mobile
if config.check_option("module", "mobile"):
build_sdk_native(base_dir + "/../sdkjs/build", False)
build_sdk_native(base_dir + "/../sdkjs/build")
base.create_dir(out_dir + "/mobile")
base.create_dir(out_dir + "/mobile/sdkjs")
vendor_dir_src = base_dir + "/../web-apps/vendor/"
@ -104,7 +109,10 @@ def make():
# JS build
def _run_npm(directory):
return base.cmd_in_dir(directory, "npm", ["install"])
retValue = base.cmd_in_dir(directory, "npm", ["install"], True)
if (0 != retValue):
retValue = base.cmd_in_dir(directory, "npm", ["install", "--verbose"])
return retValue
def _run_npm_ci(directory):
return base.cmd_in_dir(directory, "npm", ["ci"])
@ -117,7 +125,7 @@ def _run_grunt(directory, params=[]):
def build_interface(directory):
_run_npm(directory)
_run_grunt(directory, ["--force"] + base.web_apps_addons_param())
_run_grunt(directory, ["--force", "--verbose"] + base.web_apps_addons_param())
return
def get_build_param(minimize=True):

View File

@ -38,7 +38,8 @@ def make():
if(base.is_exist(custom_public_key)):
base.copy_file(custom_public_key, server_dir + '/Common/sources')
pkg_target = "node16"
#node22 packaging has issue https://github.com/yao-pkg/pkg/issues/87
pkg_target = "node20"
if ("linux" == base.host_platform()):
pkg_target += "-linux"
@ -64,10 +65,9 @@ def build_server_with_addons():
for addon in addons:
if (addon):
addon_dir = base.get_script_dir() + "/../../" + addon
base.cmd_in_dir(addon_dir, "npm", ["ci"])
base.cmd_in_dir(addon_dir, "npm", ["run", "build"])
if (base.is_exist(addon_dir)):
base.cmd_in_dir(addon_dir, "npm", ["ci"])
base.cmd_in_dir(addon_dir, "npm", ["run", "build"])
def build_server_develop():
server_dir = base.get_script_dir() + "/../../server"
base.cmd_in_dir(server_dir, "npm", ["ci"])
base.cmd_in_dir(server_dir, "grunt", ["develop", "-v"] + base.server_addons_param())
build_server_with_addons()

View File

@ -35,6 +35,18 @@ def make(solution=""):
qmake.make(platform, pro, "xcframework_platform_ios_simulator")
if config.check_option("module", "builder") and base.is_windows() and "onlyoffice" == config.branding():
# check branding libs
if (config.option("branding-name") == "onlyoffice"):
for platform in platforms:
if not platform in config.platforms:
continue
core_lib_unbranding_dir = os.getcwd() + "/../core/build/lib/" + platform + base.qt_dst_postfix()
if not base.is_dir(core_lib_unbranding_dir):
base.create_dir(core_lib_unbranding_dir)
core_lib_branding_dir = os.getcwd() + "/../core/build/onlyoffice/lib/" + platform + base.qt_dst_postfix()
base.copy_file(core_lib_branding_dir + "/doctrenderer.dll", core_lib_unbranding_dir + "/doctrenderer.dll")
base.copy_file(core_lib_branding_dir + "/doctrenderer.lib", core_lib_unbranding_dir + "/doctrenderer.lib")
# check replace
directory_builder_branding = os.getcwd() + "/../core/DesktopEditor/doctrenderer"
if base.is_dir(directory_builder_branding):

View File

@ -17,9 +17,11 @@ import websocket_all
import v8
import html2
import iwork
import md
import hunspell
import glew
import harfbuzz
import oo_brotli
import hyphen
import googletest
import libvlc
@ -44,11 +46,13 @@ def make():
v8.make()
html2.make()
iwork.make(False)
md.make()
hunspell.make(False)
harfbuzz.make()
glew.make()
hyphen.make()
googletest.make()
oo_brotli.make()
if config.check_option("build-libvlc", "1"):
libvlc.make()

View File

@ -22,10 +22,10 @@ def move_debug_libs_windows(dir):
def clean():
if base.is_dir("boost_1_58_0"):
base.delete_dir_with_access_error("boost_1_58_0");
base.delete_dir_with_access_error("boost_1_58_0")
base.delete_dir("boost_1_58_0")
if base.is_dir("boost_1_72_0"):
base.delete_dir_with_access_error("boost_1_72_0");
base.delete_dir_with_access_error("boost_1_72_0")
base.delete_dir("boost_1_72_0")
if base.is_dir("build"):
base.delete_dir("build")
@ -89,10 +89,15 @@ def make():
correct_install_includes_win(base_dir, "win_32")
if config.check_option("platform", "linux_64") and not base.is_dir("../build/linux_64"):
base.cmd("./bootstrap.sh", ["--with-libraries=filesystem,system,date_time,regex"])
addon_config = []
addon_compile = []
if "1" == config.option("use-clang"):
addon_config = ["--with-toolset=clang"]
addon_compile = ["cxxflags=-stdlib=libc++", "linkflags=-stdlib=libc++", "define=_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"]
base.cmd("./bootstrap.sh", ["--with-libraries=filesystem,system,date_time,regex"] + addon_config)
base.cmd("./b2", ["headers"])
base.cmd("./b2", ["--clean"])
base.cmd("./b2", ["--prefix=./../build/linux_64", "link=static", "cxxflags=-fPIC", "install"])
base.cmd("./b2", ["--prefix=./../build/linux_64", "link=static", "cxxflags=-fPIC"] + addon_compile + ["install"])
# TODO: support x86
if config.check_option("platform", "linux_arm64") and not base.is_dir("../build/linux_arm64"):

View File

@ -8,7 +8,7 @@ import os
def clean():
if base.is_dir("glew-2.1.0"):
base.delete_dir("glew-2.1.0");
base.delete_dir("glew-2.1.0")
return
def make():
@ -16,7 +16,7 @@ def make():
return
if not config.check_option("module", "mobile"):
return;
return
print("[fetch & build]: glew")
base_dir = base.get_script_dir() + "/../../core/Common/3dParty/glew"

View File

@ -7,7 +7,27 @@ import base
import os
import subprocess
def clear_module():
directories = ["gumbo-parser", "katana-parser"]
for dir in directories:
if base.is_dir(dir):
base.delete_dir_with_access_error(dir)
def make():
old_cur_dir = os.getcwd()
print("[fetch]: html")
base_dir = base.get_script_dir() + "/../../core/Common/3dParty/html"
os.chdir(base_dir)
base.check_module_version("2", clear_module)
os.chdir(old_cur_dir)
base.cmd_in_dir(base_dir, "python", ["fetch.py"])
return
if __name__ == '__main__':
# manual compile
make()

View File

@ -10,6 +10,8 @@ import glob
import icu_android
def fetch_icu(major, minor):
if (base.is_dir("./icu2")):
base.delete_dir_with_access_error("icu2")
base.cmd("git", ["clone", "--depth", "1", "--branch", "maint/maint-" + major, "https://github.com/unicode-org/icu.git", "./icu2"])
base.copy_dir("./icu2/icu4c", "./icu")
base.delete_dir_with_access_error("icu2")
@ -89,8 +91,13 @@ def make():
if not base.is_dir(base_dir + "/linux_64"):
base.create_dir(base_dir + "/icu/cross_build")
os.chdir("icu/cross_build")
base.cmd("./../source/runConfigureICU", ["Linux", "--prefix=" + base_dir + "/icu/cross_build_install"])
base.replaceInFile("./../source/icudefs.mk.in", "LDFLAGS = @LDFLAGS@ $(RPATHLDFLAGS)", "LDFLAGS = @LDFLAGS@ $(RPATHLDFLAGS) -static-libstdc++ -static-libgcc")
command_configure = "./../source/runConfigureICU"
command_compile_addon = "-static-libstdc++ -static-libgcc"
if "1" == config.option("use-clang"):
command_configure = "CXXFLAGS=-stdlib=libc++ " + command_configure
command_compile_addon = "-stdlib=libc++"
base.cmd(command_configure, ["Linux", "--prefix=" + base_dir + "/icu/cross_build_install"])
base.replaceInFile("./../source/icudefs.mk.in", "LDFLAGS = @LDFLAGS@ $(RPATHLDFLAGS)", "LDFLAGS = @LDFLAGS@ $(RPATHLDFLAGS) " + command_compile_addon)
base.cmd("make", ["-j4"])
base.cmd("make", ["install"], True)
base.create_dir(base_dir + "/linux_64")

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
import sys
sys.path.append('../..')
import config
import base
import os
import subprocess
def make():
print("[fetch]: md")
base_dir = base.get_script_dir() + "/../../core/Common/3dParty/md"
base.cmd_in_dir(base_dir, "python", ["fetch.py"])
return
if __name__ == '__main__':
# manual compile
make()

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python
import sys
sys.path.append('../..')
import base
import os
def make():
print("[fetch & build]: brotli")
base.cmd_in_dir(base.get_script_dir() + "/../../core/Common/3dParty/brotli", "./make.py")
return
if __name__ == '__main__':
# manual compile
make()

View File

@ -81,8 +81,16 @@ def make():
if (-1 != config.option("platform").find("linux")) and not base.is_dir("../build/linux_64"):
base.cmd("./config", ["enable-md2", "no-shared", "no-asm", "--prefix=" + old_cur_dir + "/build/linux_64", "--openssldir=" + old_cur_dir + "/build/linux_64"])
base.replaceInFile("./Makefile", "CFLAGS=-Wall -O3", "CFLAGS=-Wall -O3 -fvisibility=hidden")
base.replaceInFile("./Makefile", "CXXFLAGS=-Wall -O3", "CXXFLAGS=-Wall -O3 -fvisibility=hidden")
if "1" == config.option("use-clang"):
base.replaceInFile("./Makefile", "CC=$(CROSS_COMPILE)gcc", "CC=$(CROSS_COMPILE)clang")
base.replaceInFile("./Makefile", "CXX=$(CROSS_COMPILE)g++", "CXX=$(CROSS_COMPILE)clang++")
base.replaceInFile("./Makefile", "CFLAGS=-Wall -O3", "CFLAGS=-Wall -O3 -fvisibility=hidden")
base.replaceInFile("./Makefile", "CXXFLAGS=-Wall -O3", "CXXFLAGS=-Wall -O3 -fvisibility=hidden -stdlib=libc++")
base.replaceInFile("./Makefile", "LDFLAGS", "LDFLAGS=-stdlib=libc++")
else:
base.replaceInFile("./Makefile", "CFLAGS=-Wall -O3", "CFLAGS=-Wall -O3 -fvisibility=hidden")
base.replaceInFile("./Makefile", "CXXFLAGS=-Wall -O3", "CXXFLAGS=-Wall -O3 -fvisibility=hidden")
base.cmd("make")
base.cmd("make", ["install"])
base.cmd("make", ["clean"], True)

View File

@ -10,10 +10,10 @@ import v8_89
def clean():
if base.is_dir("depot_tools"):
base.delete_dir_with_access_error("depot_tools");
base.delete_dir_with_access_error("depot_tools")
base.delete_dir("depot_tools")
if base.is_dir("v8"):
base.delete_dir_with_access_error("v8");
base.delete_dir_with_access_error("v8")
base.delete_dir("v8")
if base.is_exist("./.gclient"):
base.delete_file("./.gclient")
@ -45,7 +45,7 @@ def is_use_clang():
gcc_version = base.get_gcc_version()
is_clang = "false"
if (gcc_version >= 6000):
if (gcc_version >= 6000 or "1" == config.option("use-clang")):
is_clang = "true"
print("gcc version: " + str(gcc_version) + ", use clang:" + is_clang)
@ -269,7 +269,7 @@ def make_xp():
"for file in projects:",
" replaceInFile(file, '<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>', '<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>')",
" replaceInFile(file, '<RuntimeLibrary>MultiThreaded</RuntimeLibrary>', '<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>')",
]);
])
programFilesDir = base.get_env("ProgramFiles")
if ("" != base.get_env("ProgramFiles(x86)")):

View File

@ -23,6 +23,9 @@ def change_bootstrap():
base.replaceInFile("./depot_tools/bootstrap/bootstrap.py",
"raise subprocess.CalledProcessError(proc.returncode, argv, None)", "return")
base.replaceInFile("./depot_tools/bootstrap/bootstrap.py",
" _win_git_bootstrap_config()", " #_win_git_bootstrap_config()")
base.writeFile("./depot_tools/bootstrap/manifest.txt", content)
return
@ -49,12 +52,20 @@ def make_args(args, platform, is_64=True, is_debug=False):
else:
args_copy.append("is_debug=false")
linux_clang = False
if (platform == "linux"):
args_copy.append("is_clang=true")
args_copy.append("use_sysroot=false")
if "1" == config.option("use-clang"):
args_copy.append("use_sysroot=true")
linux_clang = True
else:
args_copy.append("use_sysroot=false")
if (platform == "windows"):
args_copy.append("is_clang=false")
if linux_clang != True:
args_copy.append("use_custom_libcxx=false")
return "--args=\"" + " ".join(args_copy) + "\""
def ninja_windows_make(args, is_64=True, is_debug=False):
@ -158,7 +169,6 @@ def make():
"is_component_build=false",
"v8_monolithic=true",
"v8_use_external_startup_data=false",
"use_custom_libcxx=false",
"treat_warnings_as_errors=false"]
if config.check_option("platform", "linux_64"):

View File

@ -39,6 +39,7 @@ def make():
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "PdfFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "DjVuFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "XpsFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "OFDFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "Fb2File")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "EpubFile")
@ -125,7 +126,14 @@ def make():
base.mac_correct_rpath_x2t(root_dir)
base.mac_correct_rpath_docbuilder(root_dir)
base.create_x2t_js_cache(root_dir, "builder")
base.create_x2t_js_cache(root_dir, "builder", platform)
base.create_dir(root_dir + "/fonts")
base.copy_dir(git_dir + "/core-fonts/asana", root_dir + "/fonts/asana")
base.copy_dir(git_dir + "/core-fonts/caladea", root_dir + "/fonts/caladea")
base.copy_dir(git_dir + "/core-fonts/crosextra", root_dir + "/fonts/crosextra")
base.copy_dir(git_dir + "/core-fonts/openoffice", root_dir + "/fonts/openoffice")
base.copy_file(git_dir + "/core-fonts/ASC.ttf", root_dir + "/fonts/ASC.ttf")
# delete unnecessary builder files
def delete_files(files):
@ -138,6 +146,11 @@ def make():
if 0 != platform.find("mac"):
delete_files(base.find_files(root_dir, "sdk-all.js"))
delete_files(base.find_files(root_dir, "sdk-all-min.js"))
base.delete_dir(root_dir + "/sdkjs/slide/themes")
base.delete_dir(root_dir + "/sdkjs/cell/css")
base.delete_file(root_dir + "/sdkjs/pdf/src/engine/viewer.js")
base.delete_file(root_dir + "/sdkjs/common/spell/spell/spell.js.mem")
base.delete_dir(root_dir + "/sdkjs/common/Images")
return

View File

@ -32,6 +32,7 @@ def make():
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "doctrenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "DjVuFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "XpsFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "OFDFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "PdfFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "UnicodeConverter")
@ -67,7 +68,7 @@ def make():
# js cache
base.generate_doctrenderer_config(archive_dir + "/DoctRenderer.config", "./", "builder", "", "./dictionaries")
base.create_x2t_js_cache(archive_dir, "core")
base.create_x2t_js_cache(archive_dir, "core", platform)
base.delete_file(archive_dir + "/DoctRenderer.config")
# dictionaries

View File

@ -40,7 +40,7 @@ def make():
isWindowsXP = False if (-1 == native_platform.find("_xp")) else True
platform = native_platform[0:-3] if isWindowsXP else native_platform
apps_postfix = "build" + base.qt_dst_postfix();
apps_postfix = "build" + base.qt_dst_postfix()
if ("" != config.option("branding")):
apps_postfix += ("/" + config.option("branding"))
apps_postfix += "/"
@ -65,6 +65,7 @@ def make():
base.copy_lib(build_libraries_path, root_dir + "/converter", "PdfFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "DjVuFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "XpsFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "OFDFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "HtmlFile2")
base.copy_lib(build_libraries_path, root_dir + "/converter", "Fb2File")
base.copy_lib(build_libraries_path, root_dir + "/converter", "EpubFile")
@ -115,8 +116,6 @@ def make():
base.copy_dir(git_dir + "/core-fonts/openoffice", root_dir + "/fonts/openoffice")
base.copy_file(git_dir + "/core-fonts/ASC.ttf", root_dir + "/fonts/ASC.ttf")
base.copy_file(git_dir + "/desktop-apps/common/package/license/3dparty/3DPARTYLICENSE", root_dir + "/3DPARTYLICENSE")
# cef
build_dir_name = "build"
if (0 == platform.find("linux")) and (config.check_option("config", "cef_version_107")):
@ -184,6 +183,7 @@ def make():
base.copy_file(git_dir + "/desktop-apps/win-linux/extras/projicons/" + apps_postfix + "/projicons.exe", root_dir + "/DesktopEditors.exe")
if not isWindowsXP:
base.copy_file(git_dir + "/desktop-apps/win-linux/extras/update-daemon/" + apps_postfix + "/updatesvc.exe", root_dir + "/updatesvc.exe")
else:
base.copy_file(git_dir + "/desktop-apps/win-linux/extras/online-installer/" + apps_postfix + "/online-installer.exe", root_dir + "/online-installer.exe")
base.copy_file(git_dir + "/desktop-apps/win-linux/" + apps_postfix + "/DesktopEditors.exe", root_dir + "/editors.exe")
base.copy_file(git_dir + "/desktop-apps/win-linux/res/icons/desktopeditors.ico", root_dir + "/app.ico")
@ -261,7 +261,7 @@ def make():
if isUseJSC:
base.delete_file(root_dir + "/converter/icudtl.dat")
base.create_x2t_js_cache(root_dir + "/converter", "desktop")
base.create_x2t_js_cache(root_dir + "/converter", "desktop", platform)
if (0 == platform.find("win")):
base.delete_file(root_dir + "/cef_sandbox.lib")

View File

@ -57,6 +57,7 @@ def make():
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "PdfFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "DjVuFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "XpsFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "OFDFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "doctrenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "Fb2File")

View File

@ -77,6 +77,7 @@ def make():
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "PdfFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "DjVuFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "XpsFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "OFDFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "doctrenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "Fb2File")
@ -119,7 +120,7 @@ def make():
+ glob.glob(js_dir + "/web-apps/apps/*/mobile/dist/js/*.js.map"):
base.delete_file(file)
base.create_x2t_js_cache(converter_dir, "server")
base.create_x2t_js_cache(converter_dir, "server", platform)
# add embed worker code
base.cmd_in_dir(git_dir + "/sdkjs/common/embed", "python", ["make.py", js_dir + "/web-apps/apps/api/documents/api.js"])
@ -184,6 +185,7 @@ def make():
base.copy_file(license_file1, build_server_dir)
base.copy_file(license_file2, build_server_dir)
base.copy_dir(license_dir, license)
base.copy_dir(server_dir + '/dictionaries', build_server_dir + '/dictionaries')
#branding
welcome_files = branding_dir + '/welcome'

View File

@ -0,0 +1,104 @@
# This script was successfully executed on Ubuntu 22.04.5 LTS
# Before starting, make sure that:
# 1. Python >= 3.9
# 2. The current working folder with the script and its path do not contain spaces and use Latin characters.
# 3. Antivirus is turned off
# 4. There is enough free space on the disk (50GB Libre Office and during the unpacking of packages, it's recommended that you allocate at least 80 gigabytes of free space)
# 5. The current working folder with the script and its path do not contain spaces and use Latin characters.
# If the error "You must put some 'source' URIs in your sources.list" occurs, you need to run the command:
# software-properties-gtk
# in the terminal, and then under the "Ubuntu Software" tab, click "Source code" if it's not turned on and submit
# after completion, the file will appear:
# current_folder_with_script/libreoffice_build/instdir/soffice
# debugging can be done via MVS 2022
# https://wiki.documentfoundation.org/Development/IDE#Microsoft_Visual_Studio
# or via VS Code with c/c++ tools
# https://wiki.documentfoundation.org/Development/IDE#Visual_Studio_Code_(VSCode)
# or via Qt Creator
# https://wiki.documentfoundation.org/Development/IDE#Qt_Creator
# or via attatch to the soffice.bin process
# https://wiki.documentfoundation.org/Development/How_to_debug#Debugging_with_gdb
import subprocess
import sys
import os
CONFIGURE_PARAMS = [
"--enable-dbgutil",
"--without-doxygen",
"--enable-pch",
"--disable-ccache",
# "--with-visual-studio=2022",
'--enable-symbols="all"'
]
SUDO_DEPENDENCIES = [
"git", "build-essential", "zip", "ccache", "junit4", "libkrb5-dev", "nasm", "graphviz", "python3",
"python3-dev", "python3-setuptools", "qtbase5-dev", "libkf5coreaddons-dev", "libkf5i18n-dev",
"libkf5config-dev", "libkf5windowsystem-dev", "libkf5kio-dev", "libqt5x11extras5-dev", "autoconf",
"libcups2-dev", "libfontconfig1-dev", "gperf", "openjdk-17-jdk", "doxygen", "libxslt1-dev",
"xsltproc", "libxml2-utils", "libxrandr-dev", "libx11-dev", "bison", "flex", "libgtk-3-dev",
"libgstreamer-plugins-base1.0-dev", "libgstreamer1.0-dev", "ant", "ant-optional", "libnss3-dev",
"libavahi-client-dev", "libxt-dev"
]
DIR_NAME = "libreoffice"
OFFICE_PATH = "instdir/program/soffice"
class bcolors:
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
FAIL = '\033[91m'
RESET = '\033[0m'
def run_command(command, exit_on_error=True):
try:
subprocess.run(command, shell=True, check=True)
except subprocess.CalledProcessError as e:
print(f"{bcolors.FAIL}Error executing command: {command}{bcolors.RESET}")
if exit_on_error:
sys.exit(1)
def install_dependencies():
print("Updating package list...")
run_command("sudo apt update")
print("Adding PPA for GCC/G++ update...")
run_command("sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test")
run_command("sudo apt update")
print("Installing dependencies for LibreOffice...")
run_command("sudo apt-get build-dep -y libreoffice")
run_command(f"sudo apt-get install {' '.join(map(str, SUDO_DEPENDENCIES))}")
print("Updating GCC/G++ to v12...")
run_command("sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60 --slave /usr/bin/g++ g++ /usr/bin/g++-12", exit_on_error=False)
print(bcolors.OKGREEN + "All dependencies successfully installed!" + bcolors.RESET)
def build_libreoffice():
print("Cloning LibreOffice repository...")
run_command(f"git clone https://git.libreoffice.org/core {DIR_NAME}", exit_on_error=False)
print("Changing to build directory...")
os.chdir(f"./{DIR_NAME}")
print("Start configurator autogen.sh...")
run_command(f"./autogen.sh {' '.join(map(str, CONFIGURE_PARAMS))}")
print(bcolors.OKCYAN + "Starting libreoffice build, this may take up to 24 hours and takes up about 20 GB of drive space. You will also most likely need at least 8 GBs of RAM, otherwise the machine might fall into swap and appear to freeze up..." + bcolors.RESET)
run_command("make")
print(bcolors.OKGREEN + "LibreOffice build completed!" + bcolors.RESET)
# print(bcolors.OKCYAN + "Running LibreOffice..." + bcolors.RESET)
# run_command(OFFICE_PATH, exit_on_error=False)
if __name__ == "__main__":
install_dependencies()
build_libreoffice()

View File

@ -0,0 +1,202 @@
# Before starting, make sure that:
# 1. MVS 2022 is installed and the necessary individual components are in its installer
# <20> Windows Universal C Runtime
# <20> .NET Framework 4.x SDK (.NET Framework 5.x SDK and later are currently not supported. These don't register their information to registry, don't have csc.exe and they use dotnet command with csc.dll instead for compiling.)
# <20> C++ 20xx Redistributable MSMs (only required to build MSI installer)
# <20> C++ Clang Compiler for Windows (x.x.x)
# 2. Java JDK >= 17
# 3. Antivirus is turned off
# 4. There is enough free space on the disk (50GB Libre Office, 50Gb cygwin64)
# after completion, the files will appear:
# {LO_BUILD_PATH}/sources/libo-core/instdir/program/soffice.exe
# {LO_BUILD_PATH}/sources/libo-core/LibreOffice.sln
# debugging can be done via MVS 2022
# https://wiki.documentfoundation.org/Development/IDE#Microsoft_Visual_Studio
# or via attatch to the soffice.bin process
# https://wiki.documentfoundation.org/Development/How_to_debug#Debugging_with_gdb
import sys
sys.path.append('../../scripts')
import threading
import os
import subprocess
import shutil
import argparse
import base
CYGWIN_DOWNLOAD_URL = 'https://cygwin.com/setup-x86_64.exe'
CYGWIN_TEMP_PATH = './tmp'
CYGWIN_SETUP_FILENAME = 'setup-x86_64.exe'
CYGWIN_SETUP_PARAMS = [
"-P", "autoconf",
"-P", "automake",
"-P", "bison",
"-P", "cabextract",
"-P", "doxygen",
"-P", "flex",
"-P", "gawk=5.2.2-1",
"-P", "gcc-g++",
"-P", "gettext-devel",
"-P", "git",
"-P", "gnupg",
"-P", "gperf",
"-P", "make",
"-P", "mintty",
"-P", "nasm",
"-P", "openssh",
"-P", "openssl",
"-P", "patch",
"-P", "perl",
"-P", "python",
"-P", "python3",
"-P", "pkg-config",
"-P", "rsync",
"-P", "unzip",
"-P", "vim",
"-P", "wget",
"-P", "zip",
"-P", "perl-Archive-Zip",
"-P", "perl-Font-TTF",
"-P", "perl-IO-String",
"--no-admin",
"--quiet-mode"
]
CYGWIN_BAT_PATH = 'C:/cygwin64/Cygwin.bat'
LO_BUILD_PATH = os.path.normpath(os.path.join(os.getcwd(), '../../../LO'))
CONFIGURE_PARAMS = [f'--with-external-tar="{LO_BUILD_PATH}/sources/lo-externalsrc"',
f'--with-junit="{LO_BUILD_PATH}/sources/junit-4.10.jar"',
f'--with-ant-home="{LO_BUILD_PATH}/sources/apache-ant-1.9.5"',
"--enable-pch",
"--disable-ccache",
"--with-visual-studio=2022",
"--enable-dbgutil",
'--enable-symbols="all"']
def create_folder_safe(folder_path):
if not os.path.exists(folder_path):
try:
os.mkdir(folder_path)
print(f"Folder '{folder_path}' created successfully.")
except Exception as e:
print(f"Error creating folder: {e}")
else:
print(f"Folder '{folder_path}' already exists.")
class CygwinRunner:
@staticmethod
def process_commands(commands: list[str]):
proc = subprocess.Popen(
[CYGWIN_BAT_PATH], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE
)
def read_stdout():
for line in iter(proc.stdout.readline, ''):
sys.stdout.write(line)
proc.stdout.close()
def read_stderr():
for line in iter(proc.stderr.readline, ''):
sys.stderr.write(line)
proc.stderr.close()
stdout_thread = threading.Thread(target=read_stdout)
stderr_thread = threading.Thread(target=read_stderr)
stdout_thread.start()
stderr_thread.start()
for command in commands:
proc.stdin.write(command + '\n')
proc.stdin.flush()
stdout_thread.join()
stderr_thread.join()
proc.stdin.close()
proc.wait()
@staticmethod
def install_gnu_make():
base.print_info("install_gnu_make")
commands = ['mkdir -p /opt/lo/bin',
'cd /opt/lo/bin',
'wget https://dev-www.libreoffice.org/bin/cygwin/make-4.2.1-msvc.exe',
'cp make-4.2.1-msvc.exe make',
'chmod +x make',
'exit']
CygwinRunner.process_commands(commands)
@staticmethod
def install_ant_and_junit():
base.print_info("install_ant_and_junit")
commands = [f'mkdir -p {LO_BUILD_PATH}/sources',
f'cd {LO_BUILD_PATH}/sources',
'wget https://archive.apache.org/dist/ant/binaries/apache-ant-1.9.5-bin.tar.bz2',
'tar -xjvf apache-ant-1.9.5-bin.tar.bz2',
'wget http://downloads.sourceforge.net/project/junit/junit/4.10/junit-4.10.jar',
'exit']
CygwinRunner.process_commands(commands)
@staticmethod
def clone_lo():
base.print_info("clone_lo")
commands = [f'cd {LO_BUILD_PATH}/sources',
'git clone https://gerrit.libreoffice.org/core libo-core',
'exit']
CygwinRunner.process_commands(commands)
@staticmethod
def build_autogen():
base.print_info("build_autogen")
commands = [f'cd {LO_BUILD_PATH}/sources/libo-core',
f"./autogen.sh {' '.join(map(str, CONFIGURE_PARAMS))}",
'exit']
CygwinRunner.process_commands(commands)
@staticmethod
def run_make_build():
base.print_info("run_make")
commands = [f'cd {LO_BUILD_PATH}/sources/libo-core',
f'/opt/lo/bin/make gb_COLOR=1',
"exit"]
CygwinRunner.process_commands(commands)
@staticmethod
def build_vs_integration():
base.print_info("run_make")
commands = [f'cd {LO_BUILD_PATH}/sources/libo-core',
f'/opt/lo/bin/make gb_COLOR=1 vs-ide-integration',
"exit"]
CygwinRunner.process_commands(commands)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="options")
parser.add_argument("--lo_build_path", dest="build_path", default=f'../../../LO')
parser.add_argument("--disable_sln", dest="disable_sln", action=argparse.BooleanOptionalAction)
args = parser.parse_args()
LO_BUILD_PATH = args.build_path
DISABLE_SLN = args.disable_sln
create_folder_safe(f'{LO_BUILD_PATH}/sources/lo-externalsrc')
create_folder_safe(CYGWIN_TEMP_PATH)
os.chdir(CYGWIN_TEMP_PATH)
base.download(CYGWIN_DOWNLOAD_URL, CYGWIN_SETUP_FILENAME)
subprocess.run([CYGWIN_SETUP_FILENAME] + CYGWIN_SETUP_PARAMS)
os.chdir('..')
shutil.rmtree(CYGWIN_TEMP_PATH)
CygwinRunner.install_gnu_make()
CygwinRunner.install_ant_and_junit()
CygwinRunner.clone_lo()
CygwinRunner.build_autogen()
CygwinRunner.run_make_build()
if not DISABLE_SLN:
CygwinRunner.build_vs_integration()

View File

@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Git Operations Script
Provides functionality to clone repositories and create branches.
Uses existing methods from base module and integrates with release.py patterns.
"""
import sys
import argparse
import logging
from typing import Dict
# Add parent directory to path to import modules
sys.path.append('../')
import base
import config
import dependence
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class GitOperations:
"""Class to handle git clone and branch creation using existing base module methods."""
def __init__(self, branding: str = "onlyoffice", base_branch: str = "develop",
branding_url: str = "ONLYOFFICE/onlyoffice.git", branch_name: str = None,
modules: str = "core desktop builder server mobile"):
"""
Initialize GitOperations with branding configuration and configure repositories.
Args:
branding: Branding name (default: onlyoffice)
base_branch: Base branch to work from (default: develop)
branding_url: Relative path from git host base (default: ONLYOFFICE/onlyoffice.git)
branch_name: Name of the branch to create (required for branch operations)
modules: Modules to include (default: core desktop builder server mobile)
"""
self.branding = branding
self.base_branch = base_branch
self.branding_url = branding_url
self.branch_name = branch_name
self.modules = modules
self.work_dir = None
# Configure repositories immediately
self._configure()
# Update repositories after configuration
repositories = self.get_configured_repositories()
#base.update_repositories(repositories)
def create_branch(self, branch_name: str, repo_dir: str = None) -> bool:
"""
Create a new branch using base.cmd_in_dir.
Args:
branch_name: Name of the new branch
repo_dir: Repository directory (optional, uses current if not specified)
from_branch: Branch to create from (optional, uses current if not specified)
Returns:
bool: True if successful, False otherwise
"""
work_dir = repo_dir or self.work_dir
logger.info(f"Creating branch '{branch_name}' in {work_dir}")
try:
# Create and checkout new branch
base.cmd_in_dir(work_dir, "git", ["checkout", "-b", branch_name], True)
logger.info(f"Successfully created branch: {branch_name}")
return True
except SystemExit:
logger.error(f"Failed to create branch: {branch_name}")
return False
def push_branch(self, branch_name: str, repo_dir: str = None, set_upstream: bool = True) -> bool:
"""
Push a branch to remote repository using base.cmd_in_dir.
Args:
branch_name: Name of the branch to push
repo_dir: Repository directory (optional, uses current if not specified)
set_upstream: Whether to set upstream tracking (default: True)
Returns:
bool: True if successful, False otherwise
"""
work_dir = repo_dir or self.work_dir
logger.info(f"Pushing branch '{branch_name}' in {work_dir}")
try:
if set_upstream:
# Push branch and set upstream tracking
base.cmd_in_dir(work_dir, "git", ["push", "-u", "origin", branch_name], True)
else:
# Just push the branch
base.cmd_in_dir(work_dir, "git", ["push", "origin", branch_name], True)
logger.info(f"Successfully pushed branch: {branch_name}")
return True
except SystemExit:
logger.error(f"Failed to push branch: {branch_name}")
return False
def _configure(self) -> bool:
"""
Configure repositories using existing configure.py pattern from release.py.
Returns:
bool: True if successful, False otherwise
"""
logger.info(f"Configuring and cloning repositories for branch: {self.base_branch}")
try:
# Get build_tools origin and construct branding URL from git host base
build_tools_origin = base.git_get_origin()
# Extract git host base (everything up to the host)
# For https://github.com/ORG/build_tools.git -> https://github.com/
# For git@github.com:ORG/build_tools.git -> git@github.com:
if '://' in build_tools_origin: # HTTPS
host_base = build_tools_origin.split('/', 3)[0] + '//' + build_tools_origin.split('/', 3)[2] + '/'
else: # SSH
host_base = build_tools_origin.split(':', 1)[0] + ':'
branding_url = host_base + self.branding_url
logger.info(f"Build tools origin: {build_tools_origin}")
logger.info(f"Git host base: {host_base}")
logger.info(f"Using branding URL: {branding_url}")
# Check platform and dependencies like in release.py
platform = base.host_platform()
if platform == "windows":
dependence.check_pythonPath()
dependence.check_gitPath()
# Run configure.py like in release.py
configure_args = [
'configure.py',
'--branding', self.branding,
'--branding-url', branding_url,
'--branch', self.base_branch,
'--module', self.modules,
'--update', '1',
'--clean', '0'
]
base.cmd_in_dir('../../', 'python', configure_args)
# Parse configuration like in release.py
config.parse()
# Update build_tools repository
base.git_update('build_tools')
# Update branding repository
base.git_update(self.branding)
# Correct defaults (the branding repo is already updated)
config.parse_defaults()
logger.info("Successfully configured")
return True
except Exception as e:
logger.error(f"Failed to configure and clone: {e}")
return False
def get_configured_repositories(self) -> Dict:
"""Get repositories using existing base.get_repositories() pattern from release.py."""
repositories = base.get_repositories()
repositories['core-ext'] = [True, False]
repositories['build_tools'] = [True, False]
repositories[self.branding] = [True, False]
return repositories
def _iterate_repositories(self, operation_func, operation_name: str) -> bool:
"""
Iterate over all repositories and apply the given operation function.
Args:
operation_func: Function to apply to each repository (takes repo_name and repo_path)
operation_name: Name of the operation for logging
Returns:
bool: True if at least one operation succeeded, False otherwise
"""
repositories = self.get_configured_repositories()
success_count = 0
total_count = len(repositories)
for repo_name in repositories:
current_dir = repositories[repo_name][1]
repo_path = f"../../../{repo_name}" if current_dir == False else current_dir
if base.is_dir(repo_path):
if operation_func(repo_name, repo_path):
success_count += 1
else:
logger.warning(f"✗ Failed to {operation_name} in {repo_name}")
else:
logger.warning(f"Repository {repo_name} not found at {repo_path}")
logger.info(f"{operation_name.capitalize()} completed in {success_count}/{total_count} repositories")
return success_count > 0
def delete_branch(self, branch_name: str, repo_dir: str = None, force: bool = False) -> bool:
"""
Delete a branch using base.cmd_in_dir.
Args:
branch_name: Name of the branch to delete
repo_dir: Repository directory (optional, uses current if not specified)
force: Whether to force delete the branch (default: False)
Returns:
bool: True if successful, False otherwise
"""
work_dir = repo_dir or self.work_dir
logger.info(f"Deleting branch '{branch_name}' in {work_dir}")
try:
# Switch to base branch first to avoid deleting current branch
base.cmd_in_dir(work_dir, "git", ["checkout", self.base_branch], True)
# Delete local branch
delete_flag = "-D" if force else "-d"
base.cmd_in_dir(work_dir, "git", ["branch", delete_flag, branch_name], True)
logger.info(f"Successfully deleted local branch: {branch_name}")
# Delete remote branch
try:
base.cmd_in_dir(work_dir, "git", ["push", "origin", "--delete", branch_name], True)
logger.info(f"Successfully deleted remote branch: {branch_name}")
except SystemExit:
logger.warning(f"Failed to delete remote branch: {branch_name} (may not exist)")
return True
except SystemExit:
logger.error(f"Failed to delete branch: {branch_name}")
return False
def create_branches(self) -> bool:
"""
Create a branch with the given name in all repositories.
Returns:
bool: True if successful, False otherwise
"""
logger.info(f"Creating branch '{self.branch_name}' in all repositories")
def create_and_push_branch(repo_name: str, repo_path: str) -> bool:
"""Create and push branch for a single repository."""
if self.create_branch(self.branch_name, repo_path):
logger.info(f"✓ Created branch '{self.branch_name}' in {repo_name}")
# Push the created branch
if self.push_branch(self.branch_name, repo_path):
logger.info(f"✓ Pushed branch '{self.branch_name}' in {repo_name}")
return True
else:
logger.warning(f"✗ Failed to push branch '{self.branch_name}' in {repo_name}")
return False
else:
logger.warning(f"✗ Failed to create branch '{self.branch_name}' in {repo_name}")
return False
try:
return self._iterate_repositories(create_and_push_branch, f"create and push branch '{self.branch_name}'")
except Exception as e:
logger.error(f"Failed to create branch in all repositories: {e}")
return False
def remove_branches(self, force: bool = False) -> bool:
"""
Remove a branch with the given name from all repositories.
Args:
force: Whether to force delete the branch (default: False)
Returns:
bool: True if successful, False otherwise
"""
logger.info(f"Removing branch '{self.branch_name}' from all repositories")
def delete_branch_operation(repo_name: str, repo_path: str) -> bool:
"""Delete branch for a single repository."""
if self.delete_branch(self.branch_name, repo_path, force):
logger.info(f"✓ Removed branch '{self.branch_name}' from {repo_name}")
return True
else:
logger.warning(f"✗ Failed to remove branch '{self.branch_name}' from {repo_name}")
return False
try:
return self._iterate_repositories(delete_branch_operation, f"remove branch '{self.branch_name}'")
except Exception as e:
logger.error(f"Failed to remove branch from all repositories: {e}")
return False
def main():
"""Main function to handle command line arguments."""
parser = argparse.ArgumentParser(description='Git Operations Tool - Create and Remove Branches')
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Create branch command (configure, clone and create branch in all repositories)
branch_parser = subparsers.add_parser('create', help='Configure, clone and create branch in all repositories')
branch_parser.add_argument('branch_name', help='Name of the branch to create')
branch_parser.add_argument('--base-branch', default='develop', help='Base branch to work from (default: develop)')
branch_parser.add_argument('--branding', default='onlyoffice', help='Branding name')
branch_parser.add_argument('--branding-url', default='ONLYOFFICE/onlyoffice.git', help='Relative path from git host base (default: ONLYOFFICE/onlyoffice.git)')
branch_parser.add_argument('--modules', default='core desktop builder server mobile', help='Modules to include')
# Remove branch command (configure, clone and remove branch from all repositories)
remove_parser = subparsers.add_parser('remove', help='Configure, clone and remove branch from all repositories')
remove_parser.add_argument('branch_name', help='Name of the branch to remove')
remove_parser.add_argument('--base-branch', default='develop', help='Base branch to work from (default: develop)')
remove_parser.add_argument('--branding', default='onlyoffice', help='Branding name')
remove_parser.add_argument('--branding-url', default='ONLYOFFICE/onlyoffice.git', help='Relative path from git host base (default: ONLYOFFICE/onlyoffice.git)')
remove_parser.add_argument('--modules', default='core desktop builder server mobile', help='Modules to include')
remove_parser.add_argument('--force', action='store_true', help='Force delete the branch (equivalent to git branch -D)')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
git_ops = GitOperations(args.branding, args.base_branch, args.branding_url, args.branch_name, args.modules)
if args.command == 'create':
success = git_ops.create_branches()
sys.exit(0 if success else 1)
elif args.command == 'remove':
success = git_ops.remove_branches(args.force)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

View File

@ -6,6 +6,7 @@ import base
import shutil
import optparse
import dependence
import config
arguments = sys.argv[1:]
@ -17,6 +18,10 @@ parser.add_option("--remove-path", action="append", type="string", dest="remove-
(options, args) = parser.parse_args(arguments)
configOptions = vars(options)
# parse configuration
config.parse()
config.parse_defaults()
for item in configOptions["uninstall"]:
dependence.uninstallProgram(item)
for item in configOptions["remove-path"]:

View File

@ -8,6 +8,13 @@ import dependence
import traceback
import develop
# if (sys.version_info[0] >= 3):
# unicode = str
# host_platform = base.host_platform()
# if (host_platform == 'windows'):
# import libwindows
base_dir = base.get_script_dir(__file__)
def install_module(path):
@ -21,11 +28,18 @@ def find_rabbitmqctl(base_path):
return base.find_file(os.path.join(base_path, 'RabbitMQ Server'), 'rabbitmqctl.bat')
def restart_win_rabbit():
# todo maybe restarting is not relevant after many years and versions?
base.print_info('restart RabbitMQ node to prevent "Erl.exe high CPU usage every Monday morning on Windows" https://groups.google.com/forum/#!topic/rabbitmq-users/myl74gsYyYg')
rabbitmqctl = find_rabbitmqctl(os.environ['PROGRAMW6432']) or find_rabbitmqctl(os.environ['ProgramFiles(x86)'])
if rabbitmqctl is not None:
base.cmd_in_dir(base.get_script_dir(rabbitmqctl), 'rabbitmqctl.bat', ['stop_app'])
base.cmd_in_dir(base.get_script_dir(rabbitmqctl), 'rabbitmqctl.bat', ['start_app'])
try:
# code = libwindows.sudo(unicode(sys.executable), ['net', 'stop', 'rabbitmq'])
# code = libwindows.sudo(unicode(sys.executable), ['net', 'start', 'rabbitmq'])
base.cmd_in_dir(base.get_script_dir(rabbitmqctl), 'rabbitmqctl.bat', ['stop_app'])
base.cmd_in_dir(base.get_script_dir(rabbitmqctl), 'rabbitmqctl.bat', ['start_app'])
except SystemExit:
base.print_error('Perhaps Erlang cookies are different: Replace %userprofile%/.erlang.cookie with %WINDIR%/System32/config/systemprofile/.erlang.cookie')
raise
else:
base.print_info('Missing rabbitmqctl.bat')

View File

@ -198,6 +198,11 @@
"editors-ios/Vendor/ThreadSafeMutable/ThreadSafeMutableDictionary.h",
"editors-ios/Vendor/ThreadSafeMutable/ThreadSafeMutableDictionary.m"
]
},
{
"dir": "editors-webview-ios",
"fileExtensions": [".swift", ".xcconfig"],
"licensePath": "header.license",
}
]
}

View File

@ -1,5 +1,5 @@
/*
* (c) Copyright Ascensio System SIA 2010-2024
* (c) Copyright Ascensio System SIA 2010-2025
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)

32
scripts/min.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python
import sys
sys.path.append('../../build_tools/scripts')
import base
import os
args = sys.argv[1:]
if (1 > len(args)):
print("Please use min.py PATH_TO_SCRIPT.js")
exit(0)
script_path = args[0]
script_path = os.path.abspath(script_path)
script_dir = os.path.dirname(script_path)
script_name = os.path.splitext(os.path.basename(script_path))[0]
script_path_min = os.path.join(script_dir, script_name + ".min.js")
#compilation_level = "WHITESPACE_ONLY"
compilation_level = "SIMPLE_OPTIMIZATIONS"
base.cmd("java", ["-jar", "../../sdkjs/build/node_modules/google-closure-compiler-java/compiler.jar",
"--compilation_level", compilation_level,
"--js_output_file", script_path_min,
"--js", script_path])
dev_content = base.readFile(script_path)
license = dev_content[0:dev_content.find("*/")+2]
min_content = base.readFile(script_path_min)
base.delete_file(script_path_min)
base.writeFile(script_path_min, license + "\n\n" + min_content)

View File

@ -30,23 +30,6 @@ if utils.is_macos():
builder_product_name = "Document Builder"
if utils.is_linux():
builder_make_targets = [
{
"make": "tar",
"src": "tar/*.tar*",
"dst": "builder/linux/generic/"
},
{
"make": "deb",
"src": "deb/*.deb",
"dst": "builder/linux/debian/"
},
{
"make": "rpm",
"src": "rpm/build/RPMS/*/*.rpm",
"dst": "builder/linux/rhel/"
}
]
desktop_make_targets = [
{
"make": "tar",

View File

@ -15,9 +15,9 @@ def make():
if utils.is_windows():
make_windows()
elif utils.is_macos():
make_macos()
make_macos_linux()
elif utils.is_linux():
make_linux()
make_macos_linux()
return
def s3_upload(files, dst):
@ -74,15 +74,14 @@ def make_windows():
if common.clean:
utils.log_h2("builder clean")
utils.delete_dir("build")
utils.delete_files("exe\\*.exe")
utils.delete_files("zip\\*.msi")
utils.delete_dir("zip")
if make_prepare():
make_zip()
make_inno()
make_wheel()
else:
utils.set_summary("builder zip build", False)
utils.set_summary("builder inno build", False)
utils.set_summary("builder python wheel build", False)
utils.set_cwd(common.workspace_dir)
return
@ -118,33 +117,22 @@ def make_zip():
utils.set_summary("builder zip deploy", ret)
return
def make_inno():
args = [
"-Version", package_version,
"-Arch", arch
]
if not branding.onlyoffice:
args += ["-Branding", common.branding]
if common.sign:
args += ["-Sign"]
utils.log_h2("builder inno build")
ret = utils.ps1("make_inno.ps1", args, verbose=True)
utils.set_summary("builder inno build", ret)
if common.deploy and ret:
utils.log_h2("builder inno deploy")
ret = s3_upload(utils.glob_path("exe/*.exe"), "builder/win/inno/")
utils.set_summary("builder inno deploy", ret)
return
def make_macos():
def make_macos_linux():
utils.set_cwd("document-builder-package")
make_tar()
make_wheel()
utils.set_cwd(common.workspace_dir)
return
def make_tar():
utils.log_h2("builder tar build")
make_args = ["tar"]
if common.platform == "darwin_arm64":
make_args += ["-e", "UNAME_M=arm64"]
if common.platform == "linux_aarch64":
make_args += ["-e", "UNAME_M=aarch64"]
if not branding.onlyoffice:
make_args += ["-e", "BRANDING_DIR=../" + common.branding + "/document-builder-package"]
ret = utils.sh("make clean && make " + " ".join(make_args), verbose=True)
@ -152,29 +140,90 @@ def make_macos():
if common.deploy:
utils.log_h2("builder tar deploy")
ret = s3_upload(utils.glob_path("tar/*.tar.xz"), "builder/mac/generic/")
if utils.is_macos():
s3_dest = "builder/mac/generic/"
elif utils.is_linux():
s3_dest = "builder/linux/generic/"
ret = s3_upload(utils.glob_path("tar/*.tar.xz"), s3_dest)
utils.set_summary("builder tar deploy", ret)
utils.set_cwd(common.workspace_dir)
return
def make_linux():
utils.set_cwd("document-builder-package")
def make_wheel():
platform_tags = {
"windows_x64": "win_amd64",
"windows_x86": "win32",
"darwin_arm64": "macosx_11_0_arm64",
"darwin_x86_64": "macosx_10_9_x86_64",
"linux_x86_64": "manylinux_2_23_x86_64",
"linux_aarch64": "manylinux_2_23_aarch64"
}
utils.log_h2("builder build")
make_args = [t["make"] for t in branding.builder_make_targets]
if common.platform == "linux_aarch64":
make_args += ["-e", "UNAME_M=aarch64"]
if not branding.onlyoffice:
make_args += ["-e", "BRANDING_DIR=../" + common.branding + "/document-builder-package"]
ret = utils.sh("make clean && make " + " ".join(make_args), verbose=True)
utils.set_summary("builder build", ret)
if not common.platform in platform_tags: return
if common.deploy:
for t in branding.builder_make_targets:
utils.log_h2("builder " + t["make"] + " deploy")
ret = s3_upload(utils.glob_path(t["src"]), t["dst"])
utils.set_summary("builder " + t["make"] + " deploy", ret)
utils.log_h2("builder python wheel build")
builder_dir = "build"
if utils.is_linux():
builder_dir = "build/opt/onlyoffice/documentbuilder"
utils.delete_dir("python")
utils.copy_dir("../onlyoffice/build_tools/packaging/docbuilder/resources", "python")
utils.copy_dir(builder_dir, "python/docbuilder/lib")
desktop_dir = "../desktop-apps/macos/build/ONLYOFFICE.app/Contents/Resources/converter"
if utils.is_macos() and "desktop" in common.targets and utils.is_exist(desktop_dir):
for f in utils.glob_path(desktop_dir + "/*.dylib") + [desktop_dir + "/x2t"]:
utils.copy_file(f, builder_dir + "/" + utils.get_basename(f))
old_cwd = utils.get_cwd()
utils.set_cwd("python/docbuilder")
if not utils.is_file("docbuilder.py"):
utils.copy_file("lib/docbuilder.py", "docbuilder.py")
# fix docbuilder.py
content = ""
with open("docbuilder.py", "r") as file:
content = file.read()
old_line = "builder_path = os.path.dirname(os.path.realpath(__file__))"
new_line = "builder_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), \"lib\")"
content = content.replace(old_line, new_line)
with open("docbuilder.py", "w") as file:
file.write(content)
# remove unnecessary files
utils.set_cwd("lib")
utils.delete_dir("include")
utils.delete_file("build.date")
utils.delete_file("docbuilder.jar")
utils.delete_file("docbuilder.py")
if utils.is_windows():
utils.delete_file("doctrenderer.lib")
utils.delete_file("docbuilder.com.dll")
utils.delete_file("docbuilder.net.dll")
utils.delete_file("docbuilder.jni.dll")
elif utils.is_macos():
utils.delete_file("libdocbuilder.jni.dylib")
elif utils.is_linux():
utils.delete_file("libdocbuilder.jni.so")
utils.set_env("DOCBUILDER_VERSION", common.version + "." + common.build)
platform = "linux_64"
utils.set_cwd("../..")
plat_name = platform_tags[common.platform]
ret = utils.sh("python setup.py bdist_wheel --plat-name " + plat_name + " --python-tag py2.py3", verbose=True)
utils.set_summary("builder python wheel build", ret)
if common.deploy and ret:
utils.log_h2("builder python wheel deploy")
if utils.is_windows():
s3_dest = "builder/win/python/"
elif utils.is_macos():
s3_dest = "builder/mac/python/"
elif utils.is_linux():
s3_dest = "builder/linux/python/"
ret = s3_upload(utils.glob_path("dist/*.whl"), s3_dest)
utils.set_summary("builder python wheel deploy", ret)
utils.set_cwd(old_cwd)
utils.set_cwd(common.workspace_dir)
return

View File

@ -35,7 +35,7 @@ def s3_upload(files, dst):
#
def make_windows():
global package_name, package_version, arch, xp, suffix
global package_name, package_version, arch, xp
utils.set_cwd("desktop-apps\\win-linux\\package\\windows")
package_name = branding.desktop_package_name
@ -47,7 +47,6 @@ def make_windows():
"windows_x86_xp": "x86"
}[common.platform]
xp = common.platform.endswith("_xp")
suffix = arch + ("-xp" if xp else "")
if common.clean:
utils.log_h2("desktop clean")
@ -60,10 +59,21 @@ def make_windows():
utils.delete_files("data\\*.exe")
make_prepare()
make_zip()
make_inno()
make_advinst()
make_online()
if not xp:
make_zip()
make_zip("commercial")
make_inno()
make_inno("commercial")
if branding.onlyoffice:
make_inno("standalone")
make_inno("update")
make_advinst()
make_advinst("commercial")
else:
make_zip("xp")
make_inno("xp")
if common.platform == "windows_x86_xp":
make_online()
utils.set_cwd(common.workspace_dir)
return
@ -83,104 +93,79 @@ def make_prepare():
utils.set_summary("desktop prepare", ret)
return
def make_zip():
zip_file = "%s-%s-%s.zip" % (package_name, package_version, suffix)
def make_zip(edition = "opensource"):
if edition == "commercial": zip_file = "%s-Commercial-%s-%s.zip"
elif edition == "xp": zip_file = "%s-XP-%s-%s.zip"
else: zip_file = "%s-%s-%s.zip"
zip_file = zip_file % (package_name, package_version, arch)
args = [
"-Version", package_version,
"-Arch", arch
"-Arch", arch,
"-Target", edition
]
if xp:
args += ["-Target", "xp"]
# if common.sign:
# args += ["-Sign"]
utils.log_h2("desktop zip build")
utils.log_h2("desktop zip " + edition + " build")
ret = utils.ps1("make_zip.ps1", args, verbose=True)
utils.set_summary("desktop zip build", ret)
utils.set_summary("desktop zip " + edition + " build", ret)
if common.deploy and ret:
utils.log_h2("desktop zip deploy")
utils.log_h2("desktop zip " + edition + " deploy")
ret = s3_upload([zip_file], "desktop/win/generic/")
utils.set_summary("desktop zip deploy", ret)
utils.set_summary("desktop zip " + edition + " deploy", ret)
return
def make_inno():
inno_file = "%s-%s-%s.exe" % (package_name, package_version, suffix)
inno_sa_file = "%s-Standalone-%s-%s.exe" % (package_name, package_version, suffix)
inno_update_file = "%s-Update-%s-%s.exe" % (package_name, package_version, suffix)
update_wrapper = not (hasattr(branding, 'desktop_updates_skip_iss_wrapper') and branding.desktop_updates_skip_iss_wrapper)
def make_inno(edition = "opensource"):
if edition == "commercial": inno_file = "%s-Commercial-%s-%s.exe"
elif edition == "standalone": inno_file = "%s-Standalone-%s-%s.exe"
elif edition == "update": inno_file = "%s-Update-%s-%s.exe"
elif edition == "xp": inno_file = "%s-XP-%s-%s.exe"
else: inno_file = "%s-%s-%s.exe"
inno_file = inno_file % (package_name, package_version, arch)
args = [
"-Version", package_version,
"-Arch", arch
"-Arch", arch,
"-Target", edition
]
if common.sign:
args += ["-Sign"]
utils.log_h2("desktop inno build")
if xp:
ret = utils.ps1("make_inno.ps1", args + ["-Target", "xp"], verbose=True)
else:
ret = utils.ps1("make_inno.ps1", args, verbose=True)
utils.set_summary("desktop inno build", ret)
if branding.onlyoffice and not xp:
utils.log_h2("desktop inno standalone")
ret = utils.ps1("make_inno.ps1", args + ["-Target", "standalone"], verbose=True)
utils.set_summary("desktop inno standalone build", ret)
if update_wrapper:
utils.log_h2("desktop inno update build")
if xp:
ret = utils.ps1("make_inno.ps1", args + ["-Target", "xp_update"], verbose=True)
else:
ret = utils.ps1("make_inno.ps1", args + ["-Target", "update"], verbose=True)
utils.set_summary("desktop inno update build", ret)
if common.deploy:
utils.log_h2("desktop inno deploy")
ret = s3_upload([inno_file], "desktop/win/inno/")
utils.set_summary("desktop inno deploy", ret)
if branding.onlyoffice and not xp:
utils.log_h2("desktop inno standalone deploy")
ret = s3_upload([inno_sa_file], "desktop/win/inno/")
utils.set_summary("desktop inno standalone deploy", ret)
utils.log_h2("desktop inno update deploy")
if utils.is_file(inno_update_file):
ret = s3_upload([inno_update_file], "desktop/win/inno/")
elif utils.is_file(inno_file):
ret = s3_upload([inno_file], "desktop/win/inno/" + inno_update_file)
else:
ret = False
utils.set_summary("desktop inno update deploy", ret)
return
def make_advinst():
if not common.platform in ["windows_x64", "windows_x86"]:
return
advinst_file = "%s-%s-%s.msi" % (package_name, package_version, suffix)
args = [
"-Version", package_version,
"-Arch", arch
]
if common.sign:
args += ["-Sign"]
utils.log_h2("desktop advinst build")
ret = utils.ps1("make_advinst.ps1", args, verbose=True)
utils.set_summary("desktop advinst build", ret)
utils.log_h2("desktop inno " + edition + " build")
ret = utils.ps1("make_inno.ps1", args, verbose=True)
utils.set_summary("desktop inno " + edition + " build", ret)
if common.deploy and ret:
utils.log_h2("desktop advinst deploy")
utils.log_h2("desktop inno " + edition + " deploy")
ret = s3_upload([inno_file], "desktop/win/inno/")
utils.set_summary("desktop inno " + edition + " deploy", ret)
return
def make_advinst(edition = "opensource"):
if edition == "commercial": advinst_file = "%s-Commercial-%s-%s.msi"
else: advinst_file = "%s-%s-%s.msi"
advinst_file = advinst_file % (package_name, package_version, arch)
args = [
"-Version", package_version,
"-Arch", arch,
"-Target", edition
]
if common.sign:
args += ["-Sign"]
utils.log_h2("desktop advinst " + edition + " build")
ret = utils.ps1("make_advinst.ps1", args, verbose=True)
utils.set_summary("desktop advinst " + edition + " build", ret)
if common.deploy and ret:
utils.log_h2("desktop advinst " + edition + " deploy")
ret = s3_upload([advinst_file], "desktop/win/advinst/")
utils.set_summary("desktop advinst deploy", ret)
utils.set_summary("desktop advinst " + edition + " deploy", ret)
return
def make_online():
if not common.platform in ["windows_x64", "windows_x86"]:
return
online_file = "%s-%s-%s.exe" % ("OnlineInstaller", package_version, suffix)
online_file = utils.glob_file("OnlineInstaller-" + package_version + "*.exe")
utils.log_h2("desktop online installer build")
ret = utils.is_file(online_file)
utils.set_summary("desktop online installer build", ret)
@ -336,20 +321,23 @@ def make_sparkle_updates():
def make_linux():
utils.set_cwd("desktop-apps/win-linux/package/linux")
utils.log_h2("desktop build")
make_args = [t["make"] for t in branding.desktop_make_targets]
if common.platform == "linux_aarch64":
make_args += ["-e", "UNAME_M=aarch64"]
if not branding.onlyoffice:
make_args += ["-e", "BRANDING_DIR=../../../../" + common.branding + "/desktop-apps/win-linux/package/linux"]
ret = utils.sh("make clean && make " + " ".join(make_args), verbose=True)
utils.set_summary("desktop build", ret)
for edition in ["opensource", "commercial"]:
utils.log_h2("desktop " + edition + " build")
make_args = [t["make"] for t in branding.desktop_make_targets]
if edition == "commercial":
make_args += ["-e", "PACKAGE_EDITION=commercial"]
if common.platform == "linux_aarch64":
make_args += ["-e", "UNAME_M=aarch64"]
if not branding.onlyoffice:
make_args += ["-e", "BRANDING_DIR=../../../../" + common.branding + "/desktop-apps/win-linux/package/linux"]
ret = utils.sh("make clean && make " + " ".join(make_args), verbose=True)
utils.set_summary("desktop " + edition + " build", ret)
if common.deploy:
for t in branding.desktop_make_targets:
utils.log_h2("desktop " + t["make"] + " deploy")
ret = s3_upload(utils.glob_path(t["src"]), t["dst"])
utils.set_summary("desktop " + t["make"] + " deploy", ret)
if common.deploy:
for t in branding.desktop_make_targets:
utils.log_h2("desktop " + edition + " " + t["make"] + " deploy")
ret = s3_upload(utils.glob_path(t["src"]), t["dst"])
utils.set_summary("desktop " + edition + " " + t["make"] + " deploy", ret)
utils.set_cwd(common.workspace_dir)
return

View File

@ -96,6 +96,9 @@ def make(platform, project, qmake_config_addon="", is_no_errors=False):
if base.is_file(qt_dir + "/onlyoffice_qt.conf"):
build_params.append("-qtconf")
build_params.append(qt_dir + "/onlyoffice_qt.conf")
if "1" == config.option("use-clang"):
build_params.append("-spec")
build_params.append("linux-clang-libc++")
base.cmd(qmake_app, build_params)
base.correct_makefile_after_qmake(platform, makefile)
if ("1" == config.option("clean")):

View File

@ -13,7 +13,7 @@ def writeFile(path, content):
if (os.path.isfile(path)):
os.remove(path)
with open(path, "w") as file:
with open(path, "w", encoding='utf-8') as file:
file.write(content)
return
@ -160,6 +160,12 @@ class EditorApi(object):
editors_support = decoration[index_type_editors:index_type_editors_end]
if -1 == editors_support.find(self.type):
return
decoration = "\n".join(
line for line in decoration.splitlines()
if "@typeofeditors" not in line and "@see" not in line
)
# optimizations for first file
if 0 == self.numfile:
self.records.append(decoration + "\n" + code + "\n")
@ -208,7 +214,7 @@ if __name__ == "__main__":
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default="../../../onlyoffice.github.io\sdkjs-plugins\content\macros\libs/" # Default value
default="../../../web-apps/vendor/monaco/libs/" # Default value
)
args = parser.parse_args()
@ -217,7 +223,7 @@ if __name__ == "__main__":
if True == os.path.isdir(args.destination):
shutil.rmtree(args.destination, ignore_errors=True)
os.mkdir(args.destination)
convert_to_interface(["word/apiBuilder.js"], "word")
convert_to_interface(["word/apiBuilder.js", "../sdkjs-forms/apiBuilder.js"], "word")
convert_to_interface(["word/apiBuilder.js", "slide/apiBuilder.js"], "slide")
convert_to_interface(["word/apiBuilder.js", "slide/apiBuilder.js", "cell/apiBuilder.js"], "cell")
os.chdir(old_cur)

View File

@ -1,7 +1,14 @@
# Documentation Generation Guide
This guide explains how to generate documentation for Onlyoffice Builder/Plugins API using the provided Python scripts: `generate_docs_json.py`, `generate_docs_plugins_json.py`, `generate_docs_md.py`. These scripts are used to create JSON and Markdown documentation for the `apiBuilder.js` files from the word, cell, and slide editors.
This guide explains how to generate documentation for Onlyoffice Builder
and Plugins (Methods/Events) API using the following Python scripts:
- `office-api/generate_docs_json.py`
- `office-api/generate_docs_md.py`
- `plugins/generate_docs_methods_json.py`
- `plugins/generate_docs_methods_md.py`
- `plugins/generate_docs_events_json.py`
- `plugins/generate_docs_events_md.py`
## Requirements
@ -20,61 +27,112 @@ npm install
## Scripts Overview
### `generate_docs_json.py`
### `office-api/generate_docs_json.py`
This script generates JSON documentation based on the `apiBuilder.js` files.
- **Usage**:
```bash
python generate_docs_json.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the JSON documentation will be saved. If not specified, the default path is `../../../../office-js-api-declarations/office-js-api`.
- `output_path` (optional): The directory where the JSON documentation
will be saved. If not specified, the default path is
`../../../../office-js-api-declarations/office-js-api`.
### `generate_docs_plugins_json.py`
This script generates JSON documentation based on the `api_plugins.js` files.
- **Usage**:
```bash
python generate_docs_plugins_json.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the JSON documentation will be saved. If not specified, the default path is `../../../../office-js-api-declarations/office-js-api-plugins`.
### `generate_docs_md.py`
### `office-api/generate_docs_md.py`
This script generates Markdown documentation from the `apiBuilder.js` files.
- **Usage**:
```bash
python generate_docs_md.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the Markdown documentation will be saved. If not specified, the default path is `../../../../office-js-api/`.
- `output_path` (optional): The directory where the Markdown documentation
will be saved. If not specified, the default path is
`../../../../office-js-api/`.
### `plugins/generate_docs_methods_json.py`
This script generates JSON documentation based on the `api_plugins.js` files.
- **Usage**:
```bash
python generate_docs_methods_json.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the JSON documentation
will be saved. If not specified, the default path is
`../../../../office-js-api-declarations/office-js-api-plugins`.
### `plugins/generate_docs_events_json.py`
This script generates JSON documentation based on the `plugin-events.js` files.
- **Usage**:
```bash
python generate_docs_events_json.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the JSON documentation
will be saved. If not specified, the default path is
`../../../../office-js-api-declarations/office-js-api-plugins`.
### `plugins/generate_docs_methods_md.py`
This script generates Markdown documentation from the `api_plugins.js` files.
- **Usage**:
```bash
python generate_docs_methods_md.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the Markdown documentation
will be saved. If not specified, the default path is
`../../../../office-js-api/`.
### `plugins/generate_docs_events_md.py`
This script generates Markdown documentation from the `plugin-events.js` files.
- **Usage**:
```bash
python generate_docs_events_md.py output_path
```
- **Parameters**:
- `output_path` (optional): The directory where the Markdown documentation
will be saved. If not specified, the default path is
`../../../../office-js-api/`.
## Example
To generate JSON documentation with the default output path:
```bash
python generate_docs_json.py /path/to/save/json
```
To generate JSON documentation with the default output path:
```bash
python generate_docs_plugins_json.py /path/to/save/json
```
To generate Markdown documentation and specify a custom output path:
```bash
python generate_docs_md.py /path/to/save/markdown
```
## Notes
- Make sure to have all necessary permissions to run these scripts and write to the specified directories.
- Make sure to have all necessary permissions to run these scripts and write
to the specified directories.
- The output directories will be created if they do not exist.

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs/cell/api_plugins.js"]
"include": ["../../../../sdkjs/cell/plugin-events.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../sdkjs/common/base-plugin-events.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs/slide/api_plugins.js"]
"include": ["../../../../sdkjs/slide/plugin-events.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs-forms/apiPlugins.js"]
"include": ["../../../../sdkjs/word/plugin-events.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -1,399 +0,0 @@
import os
import json
import re
import shutil
import argparse
import generate_docs_json
# Configuration files
editors = [
"word",
"cell",
"slide",
"forms"
]
missing_examples = []
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def write_markdown_file(file_path, content):
with open(file_path, 'w', encoding='utf-8') as md_file:
md_file.write(content)
def remove_js_comments(text):
text = re.sub(r'^\s*//.*$', '', text, flags=re.MULTILINE) # single-line
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # multi-line
return text.strip()
def correct_description(string):
"""
Cleans up or transforms certain tags in a doclet description:
- <b> => **
- <note>...</note> => 💡 ...
- Provide a default if None.
"""
if string is None:
return 'No description provided.'
# Replace <b> tags with markdown bold
string = re.sub(r'<b>', '**', string)
string = re.sub(r'</b>', '**', string)
# Convert <note>...</note> to a little icon + text
string = re.sub(r'<note>(.*?)</note>', r'💡 \1', string, flags=re.DOTALL)
return string
def correct_default_value(value, enumerations, classes):
if value is None:
return ''
if value == True:
value = "true"
elif value == False:
value = "false"
else:
value = str(value)
return generate_data_types_markdown([value], enumerations, classes)
def remove_line_breaks(string):
return re.sub(r'[\r\n]+', ' ', string)
# Convert Array.<T> => T[] (including nested arrays).
def convert_jsdoc_array_to_ts(type_str: str) -> str:
"""
Recursively replaces 'Array.<T>' with 'T[]',
handling nested arrays like 'Array.<Array.<string>>' => 'string[][]'.
"""
pattern = re.compile(r'Array\.<([^>]+)>')
while True:
match = pattern.search(type_str)
if not match:
break
inner_type = match.group(1).strip()
# Recursively convert inner parts
inner_type = convert_jsdoc_array_to_ts(inner_type)
# Replace the outer Array.<...> with ...[]
type_str = (
type_str[:match.start()]
+ f"{inner_type}[]"
+ type_str[match.end():]
)
return type_str
def escape_text_outside_code_blocks(markdown: str) -> str:
"""
Splits content by fenced code blocks, escapes MDX-unsafe characters
(<, >, {, }) only in the text outside those code blocks.
"""
# A regex to capture fenced code blocks with ```
parts = re.split(r'(```.*?```)', markdown, flags=re.DOTALL)
# Even indices (0, 2, 4, ...) are outside code blocks,
# odd indices (1, 3, 5, ...) are actual code blocks.
for i in range(0, len(parts), 2):
# Only escape in parts outside code blocks
parts[i] = (parts[i]
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('{', '&#123;')
.replace('}', '&#125;')
)
return "".join(parts)
def get_base_type(ts_type: str) -> str:
"""
Given a TypeScript-like type (e.g. "Drawing[][]"), return the
'base' portion by stripping trailing "[]". For "Drawing[][]",
returns "Drawing". For "Array.<Drawing>", you'd convert it first
to "Drawing[]" then return "Drawing".
"""
while ts_type.endswith('[]'):
ts_type = ts_type[:-2]
return ts_type
def generate_data_types_markdown(types, enumerations, classes, root='../../'):
"""
1) Convert each raw JSDoc type from Array.<T> to T[].
2) Split union types if needed (usually they're provided as separate
elements in 'types' already, but let's be safe).
3) For each type, extract the base type (e.g. "Drawing" from "Drawing[]").
4) If the base type matches an enumeration or class, link the entire
T[]-based string.
5) Join with " | ".
"""
# Convert each raw type from JSDoc to TS
converted = [convert_jsdoc_array_to_ts(t) for t in types] # e.g. ["Drawing[]", "Foo[]", ...]
# For each converted type (like "Drawing[]"), see if the base is in enumerations or classes
def link_if_known(ts_type):
base = get_base_type(ts_type) # e.g. "Drawing" from "Drawing[]"
# Check enumerations first
for enum in enumerations:
if enum['name'] == base:
# Replace the entire token with a link
return f"[{ts_type}]({root}Enumeration/{base}.md)"
# Check classes
if base in classes:
return f"[{ts_type}]({root}{base}/{base}.md)"
# Otherwise just return as-is
return ts_type
# Build final list of possibly-linked types
linked = [link_if_known(ts_t) for ts_t in converted]
# Join them with " | "
param_types_md = ' | '.join(linked)
# If there's still leftover angle brackets for generics, gently escape or link them
# e.g. "Object.<string, number>" => "Object.&lt;string, number&gt;"
# or do more specialized linking if you want to handle them deeper.
def replace_leftover_generics(match):
element = match.group(1).strip()
return f"&lt;{element}&gt;"
param_types_md = re.sub(r'<([^<>]+)>', replace_leftover_generics, param_types_md)
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 += generate_properties_markdown(properties, enumerations, classes)
content += "## Methods\n\n"
for method in methods:
method_name = method['name']
content += f"- [{method_name}](./Methods/{method_name}.md)\n"
# Escape just before returning
return escape_text_outside_code_blocks(content)
def generate_method_markdown(method, enumerations, classes):
method_name = method['name']
description = method.get('description', 'No description provided.')
description = correct_description(description)
params = method.get('params', [])
returns = method.get('returns', [])
example = method.get('example', '')
memberof = method.get('memberof', '')
content = f"# {method_name}\n\n{description}\n\n"
# Syntax
param_list = ', '.join([param['name'] for param in params]) if params else ''
content += 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"
# Parameters
content += "## Parameters\n\n"
if params:
content += "| **Name** | **Required/Optional** | **Data type** | **Default** | **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.')))
param_required = "Required" if not param.get('optional') else "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"
# Returns
content += "\n## 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."
# 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\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\n{cleaned_example}\n```\n"
return escape_text_outside_code_blocks(content)
def generate_properties_markdown(properties, enumerations, classes, root='../'):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | Description |\n"
content += "| ---- | ---- | ----------- |\n"
for prop in properties:
prop_name = prop['name']
prop_description = prop.get('description', 'No description provided.')
prop_description = remove_line_breaks(correct_description(prop_description))
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"
# Escape outside code blocks
return escape_text_outside_code_blocks(content)
def generate_enumeration_markdown(enumeration, enumerations, classes):
enum_name = enumeration['name']
description = enumeration.get('description', 'No description provided.')
description = correct_description(description)
example = enumeration.get('example', '')
content = f"# {enum_name}\n\n{description}\n\n"
ptype = enumeration['type']['parsedType']
if ptype['type'] == 'TypeUnion':
enum_empty = True # is empty enum
content += "## Type\n\nEnumeration\n\n"
content += "## 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)
# Attempt linking: we compare the raw type to enumerations/classes
if any(enum['name'] == raw_t for enum in enumerations):
content += f"- [{ts_t}](../Enumeration/{raw_t}.md)\n"
enum_empty = False
elif raw_t in classes:
content += f"- [{ts_t}](../{raw_t}/{raw_t}.md)\n"
enum_empty = False
elif ts_t.find('Api') == -1:
content += f"- {ts_t}\n"
enum_empty = False
if enum_empty == True:
return None
elif enumeration['properties'] is not None:
content += "## Type\n\nObject\n\n"
content += generate_properties_markdown(enumeration['properties'], enumerations, classes)
else:
content += "## 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)
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\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\n{cleaned_example}\n```\n"
return escape_text_outside_code_blocks(content)
def process_doclets(data, output_dir, editor_name):
classes = {}
classes_props = {}
enumerations = []
editor_dir = os.path.join(output_dir, editor_name)
for doclet in data:
if doclet['kind'] == 'class':
class_name = doclet['name']
classes[class_name] = []
classes_props[class_name] = doclet.get('properties', None)
elif doclet['kind'] == 'function':
class_name = doclet.get('memberof')
if class_name:
if class_name not in classes:
classes[class_name] = []
classes[class_name].append(doclet)
elif doclet['kind'] == 'typedef':
enumerations.append(doclet)
# Process classes
for class_name, methods in classes.items():
class_dir = os.path.join(editor_dir, class_name)
methods_dir = os.path.join(class_dir, 'Methods')
os.makedirs(methods_dir, exist_ok=True)
# Write class file
class_content = generate_class_markdown(
class_name,
methods,
classes_props[class_name],
enumerations,
classes
)
write_markdown_file(os.path.join(class_dir, f"{class_name}.md"), class_content)
# Write method files
for method in methods:
method_file_path = os.path.join(methods_dir, f"{method['name']}.md")
method_content = generate_method_markdown(method, enumerations, classes)
write_markdown_file(method_file_path, method_content)
if not method.get('example', ''):
missing_examples.append(os.path.relpath(method_file_path, output_dir))
# Process enumerations
enum_dir = os.path.join(editor_dir, 'Enumeration')
os.makedirs(enum_dir, exist_ok=True)
for enum in enumerations:
enum_file_path = os.path.join(enum_dir, f"{enum['name']}.md")
enum_content = generate_enumeration_markdown(enum, enumerations, classes)
if enum_content is None:
continue
write_markdown_file(enum_file_path, enum_content)
if not enum.get('example', ''):
missing_examples.append(os.path.relpath(enum_file_path, output_dir))
def generate(output_dir):
print('Generating Markdown documentation...')
generate_docs_json.generate(output_dir + 'tmp_json', md=True)
for editor_name in editors:
input_file = os.path.join(output_dir + 'tmp_json', editor_name + ".json")
shutil.rmtree(output_dir + f'/{editor_name.title()}')
os.makedirs(output_dir + f'/{editor_name.title()}')
data = load_json(input_file)
process_doclets(data, output_dir, editor_name.title())
shutil.rmtree(output_dir + 'tmp_json')
print('Done')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate documentation")
parser.add_argument(
"destination",
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default="../../../../office-js-api/" # Default value
)
args = parser.parse_args()
generate(args.destination)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -6,11 +6,10 @@ import re
# Configuration files
configs = [
"./config/plugins/common.json",
"./config/plugins/word.json",
"./config/plugins/cell.json",
"./config/plugins/slide.json",
"./config/plugins/forms.json"
"./config/plugins/events/common.json",
"./config/plugins/events/word.json",
"./config/plugins/events/cell.json",
"./config/plugins/events/slide.json"
]
root = '../../../..'
@ -105,7 +104,7 @@ if __name__ == "__main__":
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default=f"{root}/office-js-api-declarations/office-js-api-plugins"
default=f"{root}/office-js-api-declarations/office-js-api-plugins/events"
)
args = parser.parse_args()
generate(args.destination)

View File

@ -0,0 +1,356 @@
#!/usr/bin/env python3
import os
import json
import re
import shutil
import argparse
import generate_docs_plugins_events_json
# Папки для каждого editor_name
editors = {
"word": "text-document-api",
"cell": "spreadsheet-api",
"slide": "presentation-api"
}
missing_examples = []
used_enumerations = set()
def load_json(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def write_markdown_file(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
def remove_js_comments(text):
text = re.sub(r'^\s*//.*$', '', text, flags=re.MULTILINE)
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
return text.strip()
def correct_description(string, root=''):
"""
Cleans or transforms specific tags in the doclet description:
- <b> => ** (bold text)
- <note>...</note> => 💡 ...
- {@link ...} is replaced with a Markdown link
- If the description is missing, returns a default value.
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
# Line breaks
string = string.replace('\r', '\\\n')
# Replace <b> tags with Markdown bold formatting
string = re.sub(r'<b>', '-**', string)
string = re.sub(r'</b>', '**', string)
# Replace <note> tags with an icon and text
string = re.sub(r'<note>(.*?)</note>', r'💡 \1', string, flags=re.DOTALL)
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
def process_link_tags(text, root=''):
"""
Finds patterns like {@link ...} and replaces them with Markdown links.
If the prefix 'global#' is found, a link to a typedef is generated,
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/manifest/',
'/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"
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('/plugins/methods/'):
url = f'../../{ref.split('/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)
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)"
else:
# Handle links to class methods like ClassName#MethodName
try:
class_name, method_name = ref.split("#")
except ValueError:
return match.group(0)
display_text = label if label else ref # Keep the full notation, e.g., "Api#CreateSlide"
return f"[{display_text}]({root}{class_name}/Methods/{method_name}.md)"
return re.sub(r'{@link\s+([^}]+)}', replace_link, text)
def remove_line_breaks(s):
return re.sub(r'[\r\n]+', ' ', s)
def convert_jsdoc_array_to_ts(type_str):
p = re.compile(r'Array\.<([^>]+)>')
while True:
m = p.search(type_str)
if not m:
break
inner = convert_jsdoc_array_to_ts(m.group(1).strip())
type_str = type_str[:m.start()] + inner + '[]' + type_str[m.end():]
return type_str
def generate_data_types_markdown(types, enumerations, root=''):
converted = [convert_jsdoc_array_to_ts(t) for t in types]
primitives = {"string", "number", "boolean", "null", "undefined", "any", "object", "true", "false"}
result = []
enum_names = {e['name'] for e in enumerations}
for t in converted:
base = t.rstrip('[]')
dims = t[len(base):]
if base in enum_names:
used_enumerations.add(base)
link = f"[{base}]({root}Enumeration/{base}.md)"
elif base in primitives or re.match(r"^['\"].*['\"]$", base) or re.match(r"^-?\d+(\.\d+)?$", base):
link = base
else:
link = base
result.append(link + dims)
return " | ".join(result)
def escape_text_outside_code_blocks(md):
parts = re.split(r'(```.*?```)', md, flags=re.DOTALL)
for i in range(0, len(parts), 2):
parts[i] = parts[i].replace('<', '&lt;').replace('>', '&gt;')
return "".join(parts)
def generate_event_markdown(event, enumerations):
name = event['name']
desc = correct_description(event.get('description', ''))
params = event.get('params', [])
md = f"# {name}\n\n{desc}\n\n"
# Parameters
md += "## Parameters\n\n"
if params:
md += "| **Name** | **Data type** | **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', '')))
md += f"| {p['name']} | {t_md} | {d} |\n"
md += "\n"
else:
md += "This event has no parameters.\n\n"
for ex in event.get('examples', []):
code = remove_js_comments(ex).strip()
md += f"```javascript\n{code}\n```\n\n"
return escape_text_outside_code_blocks(md)
def generate_enumeration_markdown(enumeration, enumerations):
"""
Generates Markdown documentation for a 'typedef' doclet.
This version only works with `enumeration['examples']` (an array of strings),
ignoring any single `enumeration['examples']` field.
"""
enum_name = enumeration['name']
if enum_name not in used_enumerations:
return None
description = enumeration.get('description', 'No description provided.')
description = correct_description(description, '../')
# Only use the 'examples' array
examples = enumeration.get('examples', [])
content = f"# {enum_name}\n\n{description}\n\n"
parsed_type = enumeration['type'].get('parsedType')
if not parsed_type:
# If parsedType is missing, just list 'type.names' if available
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## Type\n\n"
t_md = generate_data_types_markdown(type_names, enumerations)
content += t_md + "\n\n"
else:
ptype = parsed_type['type']
# 1) Handle TypeUnion
if ptype == 'TypeUnion':
content += "## Type\n\nEnumeration\n\n"
content += "## Values\n\n"
for raw_t in enumeration['type']['names']:
# Attempt linking
if any(enum['name'] == raw_t for enum in enumerations):
used_enumerations.add(raw_t)
content += f"- [{raw_t}](../Enumeration/{raw_t}.md)\n"
else:
content += f"- {raw_t}\n"
# 2) Handle TypeApplication (e.g. Object.<string, string>)
elif ptype == 'TypeApplication':
content += "## Type\n\nObject\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"
# 3) If properties are present, treat it like an object
if enumeration.get('properties') is not None:
content += generate_properties_markdown(enumeration['properties'], enumerations)
# 4) If it's neither TypeUnion nor TypeApplication, just output the type names
if ptype not in ('TypeUnion', 'TypeApplication'):
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## 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"
else:
content += "\n\n## Example\n\n"
for i, ex_line in enumerate(examples, start=1):
# Remove JS comments
cleaned_example = remove_js_comments(ex_line).strip()
# Attempt splitting if the user used ```js
if '```js' in cleaned_example:
comment, code = cleaned_example.split('```js', 1)
comment = comment.strip()
code = code.strip()
if len(examples) > 1:
content += f"**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"
# No special fences, just show as code
content += f"```javascript\n{cleaned_example}\n```\n"
return escape_text_outside_code_blocks(content)
def generate_properties_markdown(properties, enumerations, root='../'):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | 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))
prop_types = prop['type']['names'] if prop.get('type') else []
param_types_md = generate_data_types_markdown(prop_types, enumerations, root)
content += f"| {prop_name} | {param_types_md} | {prop_description} |\n"
# Escape outside code blocks
return escape_text_outside_code_blocks(content)
def process_events(data, events_root):
enumerations = []
events = []
for doclet in data:
kind = doclet.get('kind')
if kind == 'typedef':
enumerations.append(doclet)
elif kind == 'event':
events.append(doclet)
os.makedirs(events_root, exist_ok=True)
used_enumerations.clear()
# пишем события
for ev in events:
path = os.path.join(events_root, f"{ev['name']}.md")
write_markdown_file(path, generate_event_markdown(ev, enumerations))
if not ev.get('examples'):
missing_examples.append(os.path.relpath(path, events_root))
# пишем перечисления, используемые событиями
enum_dir = os.path.join(events_root, 'Enumeration')
os.makedirs(enum_dir, exist_ok=True)
prev = -1
while len(used_enumerations) != prev:
prev = len(used_enumerations)
for e in enumerations:
if e['name'] in used_enumerations:
generate_enumeration_markdown(e, enumerations)
for e in enumerations:
if e['name'] in used_enumerations:
path = os.path.join(enum_dir, f"{e['name']}.md")
write_markdown_file(path, generate_enumeration_markdown(e, enumerations))
if not e.get('examples'):
missing_examples.append(os.path.relpath(path, events_root))
def generate_events(output_dir):
if output_dir.endswith('/'):
output_dir = output_dir[:-1]
tmp = os.path.join(output_dir, 'tmp_json')
shutil.rmtree(output_dir, ignore_errors=True)
generate_docs_plugins_events_json.generate(tmp, md=True)
for editor_name, folder in editors.items():
data = load_json(os.path.join(tmp, f"{editor_name}.json"))
process_events(data, os.path.join(output_dir, folder))
shutil.rmtree(tmp)
print("Done. Missing examples:", missing_examples)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate events documentation")
parser.add_argument(
"destination",
nargs="?",
default="../../../../api.onlyoffice.com/site/docs/plugin-and-macros/interacting-with-editors/events/",
help="Output directory"
)
args = parser.parse_args()
generate_events(args.destination)

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs/word/apiBuilder.js", "../../../../sdkjs/slide/apiBuilder.js", "../../../../sdkjs/cell/apiBuilder.js"]
"include": ["../../../../../sdkjs/word/apiBuilder.js", "../../../../../sdkjs/slide/apiBuilder.js", "../../../../../sdkjs/cell/apiBuilder.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -82,6 +82,11 @@ exports.handlers = {
doclet.longname = cleanName(doclet.longname);
doclet.name = cleanName(doclet.name);
// skip inherited methods if ovveriden in child class
if (doclet.inherited && filteredDoclets.find((addedDoclet) => addedDoclet['name'] == doclet['name'] && addedDoclet['memberof'] == doclet['memberof'])) {
continue;
}
const filteredDoclet = {
comment: doclet.comment,
description: doclet.description,

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs/word/apiBuilder.js", "../../../../sdkjs-forms/apiBuilder.js"]
"include": ["../../../../../sdkjs/word/apiBuilder.js", "../../../../../sdkjs-forms/apiBuilder.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs/word/apiBuilder.js"]
"include": ["../../../../../sdkjs/pdf/apiBuilder.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -1,6 +1,6 @@
{
"source": {
"include": ["../../../../sdkjs/word/apiBuilder.js", "../../../../sdkjs/slide/apiBuilder.js"]
"include": ["../../../../../sdkjs/word/apiBuilder.js", "../../../../../sdkjs/slide/apiBuilder.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/word/apiBuilder.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -5,14 +5,14 @@ import argparse
import re
import platform
root = '../../../..'
root = '../../../../..'
# Configuration files
configs = [
"./config/builder/word.json",
"./config/builder/cell.json",
"./config/builder/slide.json",
"./config/builder/forms.json"
"./config/word.json",
"./config/cell.json",
"./config/slide.json",
"./config/forms.json"
]
editors_maps = {

View File

@ -0,0 +1,585 @@
import os
import json
import re
import shutil
import argparse
import generate_docs_json
# Configuration files
editors = {
"word": "text-document-api",
"cell": "spreadsheet-api",
"slide": "presentation-api",
"forms": "form-api"
}
missing_examples = []
used_enumerations = set()
cur_editor_name = None
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def write_markdown_file(file_path, content):
with open(file_path, 'w', encoding='utf-8') as md_file:
md_file.write(content)
def remove_js_comments(text):
text = re.sub(r'^\s*//.*$', '', text, flags=re.MULTILINE) # single-line
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # multi-line
return text.strip()
def process_link_tags(text, root=''):
"""
Finds patterns like {@link ...} and replaces them with Markdown links.
If the prefix 'global#' is found, a link to a typedef is generated,
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"
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)
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)"
else:
# Handle links to class methods like ClassName#MethodName
try:
class_name, method_name = ref.split("#")
except ValueError:
return match.group(0)
display_text = label if label else ref # Keep the full notation, e.g., "Api#CreateSlide"
return f"[{display_text}]({root}{class_name}/Methods/{method_name}.md)"
return re.sub(r'{@link\s+([^}]+)}', replace_link, text)
def correct_description(string, root='', isInTable=False):
"""
Cleans or transforms specific tags in the doclet description:
- <b> => ** (bold text)
- <note>...</note> => 💡 ...
- {@link ...} is replaced with a Markdown link
- If the description is missing, returns a default value.
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
if False == isInTable:
# Line breaks
string = string.replace('\r', '\\\n')
# Replace <b> tags with Markdown bold formatting
string = re.sub(r'<b>', '-**', string)
else:
string = re.sub(r'<b>', '**', string)
string = re.sub(r'</b>', '**', string)
# Replace <note> tags with an icon and text
string = re.sub(r'<note>(.*?)</note>', r'💡 \1', string, flags=re.DOTALL)
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
def correct_default_value(value, enumerations, classes):
if value is None or value == '':
return ''
if isinstance(value, bool):
value = "true" if value else "false"
else:
value = str(value)
return generate_data_types_markdown([value], enumerations, classes)
def remove_line_breaks(string):
return re.sub(r'[\r\n]+', ' ', string)
# Convert Array.<T> => T[] (including nested arrays).
def convert_jsdoc_array_to_ts(type_str: str) -> str:
"""
Recursively replaces 'Array.<T>' with 'T[]',
handling nested arrays like 'Array.<Array.<string>>' => 'string[][]'.
"""
pattern = re.compile(r'Array\.<([^>]+)>')
while True:
match = pattern.search(type_str)
if not match:
break
inner_type = match.group(1).strip()
# Recursively convert inner parts
inner_type = convert_jsdoc_array_to_ts(inner_type)
# Replace the outer Array.<...> with ...[]
type_str = (
type_str[:match.start()]
+ f"{inner_type}[]"
+ type_str[match.end():]
)
return type_str
def escape_text_outside_code_blocks(markdown: str) -> str:
"""
Splits content by fenced code blocks, escapes MDX-unsafe characters
(<, >, {, }) only in the text outside those code blocks.
"""
# A regex to capture fenced code blocks with ```
parts = re.split(r'(```.*?```)', markdown, flags=re.DOTALL)
# Even indices (0, 2, 4, ...) are outside code blocks,
# odd indices (1, 3, 5, ...) are actual code blocks.
for i in range(0, len(parts), 2):
text = (parts[i]
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('{', '&#123;')
.replace('}', '&#125;'))
parts[i] = escape_brackets_in_quotes(text)
return "".join(parts)
def escape_brackets_in_quotes(text: str) -> str:
return re.sub(
r"(['\"])(.*?)(?<!\\)\1",
lambda m: m.group(1)
+ m.group(2).replace('[', r'\[').replace(']', r'\]')
+ m.group(1),
text
)
def get_base_type(ts_type: str) -> str:
"""
Given a TypeScript-like type (e.g. "Drawing[][]"), return the
'base' portion by stripping trailing "[]". For "Drawing[][]",
returns "Drawing". For "Array.<Drawing>", you'd convert it first
to "Drawing[]" then return "Drawing".
"""
while ts_type.endswith('[]'):
ts_type = ts_type[:-2]
return ts_type
def generate_data_types_markdown(types, enumerations, classes, root='../../'):
"""
1) Converts each type from JSDoc (e.g., Array.<T>) to T[].
2) Processes union types by splitting them using '|'.
3) Supports multidimensional arrays, e.g., (string|ApiRange|number)[].
4) If the base type matches the name of an enumeration or class, generates a link.
5) The final types are joined using " | ".
"""
# Convert each type from JSDoc format to TypeScript format (e.g., T[])
converted = [convert_jsdoc_array_to_ts(t) for t in types]
# Set of primitive types
primitive_types = {"string", "number", "boolean", "null", "undefined", "any", "object", "false", "true", "json", "function", "date", "{}"}
def is_primitive(type):
if (type.lower() in primitive_types or
(type.startswith('"') and type.endswith('"')) or
(type.startswith("'") and type.endswith("'")) or
type.replace('.', '', 1).isdigit() or
(type.startswith('-') and type[1:].replace('.', '', 1).isdigit())):
return True
return False
def link_if_known(ts_type):
ts_type = ts_type.strip()
# Count the number of array dimensions, e.g., "[][]" has 2 dimensions
array_dims = 0
while ts_type.endswith("[]"):
array_dims += 1
ts_type = ts_type[:-2].strip()
# Process generic types, e.g., Object.<string, editorType>
if ".<" in ts_type and ts_type.endswith(">"):
import re
m = re.match(r'^(.*?)\.<(.*)>$', ts_type)
if m:
base_part = m.group(1).strip()
generic_args_str = m.group(2).strip()
# Process the base part of the type
found = False
for enum in enumerations:
if enum['name'] == base_part:
used_enumerations.add(base_part)
base_result = f"[{base_part}]({root}Enumeration/{base_part}.md)"
found = True
break
if not found:
if base_part in classes:
base_result = f"[{base_part}]({root}{base_part}/{base_part}.md)"
elif is_primitive(base_part):
base_result = base_part
elif cur_editor_name == "forms":
base_result = f"[{base_part}]({root}../text-document-api/{base_part}/{base_part}.md)"
else:
print(f"Unknown type encountered: {base_part}")
base_result = base_part
# Split the generic parameters by commas and process each recursively
generic_args = [link_if_known(x) for x in generic_args_str.split(",")]
result = base_result + ".&lt;" + ", ".join(generic_args) + "&gt;"
result += "[]" * array_dims
return result
# Process union types: if the type is enclosed in parentheses
if ts_type.startswith("(") and ts_type.endswith(")"):
inner = ts_type[1:-1].strip()
subtypes = [sub.strip() for sub in inner.split("|")]
if len(subtypes) == 1:
result = link_if_known(subtypes[0])
else:
processed = [link_if_known(subtype) for subtype in subtypes]
result = "(" + " | ".join(processed) + ")"
result += "[]" * array_dims
return result
# If not a generic or union type process the base type
else:
base = ts_type
found = False
for enum in enumerations:
if enum['name'] == base:
used_enumerations.add(base)
result = f"[{base}]({root}Enumeration/{base}.md)"
found = True
break
if not found:
if base in classes:
result = f"[{base}]({root}{base}/{base}.md)"
elif is_primitive(base):
result = base
elif cur_editor_name == "forms":
result = f"[{base}]({root}../text-document-api/{base}/{base}.md)"
else:
print(f"Unknown type encountered: {base}")
result = base
result += "[]" * array_dims
return result
# Apply link_if_known to each converted type
linked = [link_if_known(ts_t) for ts_t in converted]
# Join results using " | "
param_types_md = r' | '.join(linked)
param_types_md = param_types_md.replace("|", r"\|")
# Escape remaining angle brackets for generics
def replace_leftover_generics(match):
element = match.group(1).strip()
return f"&lt;{element}&gt;"
param_types_md = re.sub(r'<([^<>]+)>', replace_leftover_generics, param_types_md)
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 += generate_properties_markdown(properties, enumerations, classes)
content += "\n## Methods\n\n"
content += "| Method | Returns | Description |\n"
content += "| ------ | ------- | ----------- |\n"
for method in sorted(methods, key=lambda m: m['name']):
method_name = method['name']
# Get the type of return values
returns = method.get('returns', [])
if returns:
return_type_list = returns[0].get('type', {}).get('names', [])
returns_markdown = generate_data_types_markdown(return_type_list, enumerations, classes, '../')
else:
returns_markdown = "None"
# Processing the method description
description = remove_line_breaks(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)"
content += f"| {method_link} | {returns_markdown} | {description} |\n"
return escape_text_outside_code_blocks(content)
def generate_method_markdown(method, enumerations, classes, example_editor_name):
method_name = method['name']
description = method.get('description', 'No description provided.')
description = correct_description(description, '../../')
params = method.get('params', [])
returns = method.get('returns', [])
example = method.get('example', '')
memberof = method.get('memberof', '')
content = f"# {method_name}\n\n{description}\n\n"
# 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"
if memberof:
content += f"`expression` - A variable that represents a [{memberof}](../{memberof}.md) class.\n\n"
# Parameters
content += "## Parameters\n\n"
if params:
content += "| **Name** | **Required/Optional** | **Data type** | **Default** | **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_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"
# Returns
content += "\n## 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."
# 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"
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"
return escape_text_outside_code_blocks(content)
def generate_properties_markdown(properties, enumerations, classes, root='../'):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | 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_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"
# Escape outside code blocks
return escape_text_outside_code_blocks(content)
def generate_enumeration_markdown(enumeration, enumerations, classes, example_editor_name):
enum_name = enumeration['name']
if enum_name not in used_enumerations:
return None
description = enumeration.get('description', 'No description provided.')
description = correct_description(description, '../')
example = enumeration.get('example', '')
content = f"# {enum_name}\n\n{description}\n\n"
ptype = enumeration['type']['parsedType']
if ptype['type'] == 'TypeUnion':
enum_empty = True # is empty enum
content += "## Type\n\nEnumeration\n\n"
content += "## 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)
# Attempt linking: we compare the raw type to enumerations/classes
if any(enum['name'] == raw_t for enum in enumerations):
used_enumerations.add(raw_t)
content += f"- [{ts_t}](../Enumeration/{raw_t}.md)\n"
enum_empty = False
elif raw_t in classes:
content += f"- [{ts_t}](../{raw_t}/{raw_t}.md)\n"
enum_empty = False
elif ts_t.find('Api') == -1:
content += f"- {ts_t}\n"
enum_empty = False
if enum_empty == True:
return None
elif enumeration['properties'] is not None:
content += "## Type\n\nObject\n\n"
content += generate_properties_markdown(enumeration['properties'], enumerations, classes)
else:
content += "## 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)
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"
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"
return escape_text_outside_code_blocks(content)
def process_doclets(data, output_dir, editor_name):
global cur_editor_name
cur_editor_name = editor_name
classes = {}
classes_props = {}
enumerations = []
editor_dir = os.path.join(output_dir, editors[editor_name])
example_editor_name = 'editor-'
if editor_name == 'word':
example_editor_name += 'docx'
elif editor_name == 'forms':
example_editor_name += 'pdf'
elif editor_name == 'slide':
example_editor_name += 'pptx'
elif editor_name == 'cell':
example_editor_name += 'xlsx'
for doclet in data:
if doclet['kind'] == 'class':
class_name = doclet['name']
if class_name:
if class_name not in classes:
classes[class_name] = []
classes_props[class_name] = doclet.get('properties', None)
elif doclet['kind'] == 'function':
class_name = doclet.get('memberof')
if class_name:
if class_name not in classes:
classes[class_name] = []
classes[class_name].append(doclet)
elif doclet['kind'] == 'typedef':
enumerations.append(doclet)
# Process classes
for class_name, methods in classes.items():
if (len(methods) == 0):
continue
class_dir = os.path.join(editor_dir, class_name)
methods_dir = os.path.join(class_dir, 'Methods')
os.makedirs(methods_dir, exist_ok=True)
# Write class file
class_content = generate_class_markdown(
class_name,
methods,
classes_props[class_name],
enumerations,
classes
)
write_markdown_file(os.path.join(class_dir, f"{class_name}.md"), class_content)
# Write method files
for method in methods:
method_file_path = os.path.join(methods_dir, f"{method['name']}.md")
method_content = generate_method_markdown(method, enumerations, classes, example_editor_name)
write_markdown_file(method_file_path, method_content)
if not method.get('example', ''):
missing_examples.append(os.path.relpath(method_file_path, output_dir))
# Process enumerations
enum_dir = os.path.join(editor_dir, 'Enumeration')
os.makedirs(enum_dir, exist_ok=True)
# idle run
prev_used_count = -1
while len(used_enumerations) != prev_used_count:
prev_used_count = len(used_enumerations)
for enum in [e for e in enumerations if e['name'] in used_enumerations]:
enum_content = generate_enumeration_markdown(enum, enumerations, classes, example_editor_name)
for enum in enumerations:
enum_file_path = os.path.join(enum_dir, f"{enum['name']}.md")
enum_content = generate_enumeration_markdown(enum, enumerations, classes, example_editor_name)
if enum_content is None:
continue
write_markdown_file(enum_file_path, enum_content)
if not enum.get('example', ''):
missing_examples.append(os.path.relpath(enum_file_path, output_dir))
def generate(output_dir):
print('Generating Markdown documentation...')
generate_docs_json.generate(output_dir + 'tmp_json', md=True)
for editor_name, folder_name in editors.items():
input_file = os.path.join(output_dir + '/tmp_json', editor_name + ".json")
editor_folder_path = os.path.join(output_dir, folder_name)
for folder_name in os.listdir(editor_folder_path):
folder_path_to_del = os.path.join(editor_folder_path, folder_name)
if os.path.isdir(folder_path_to_del):
shutil.rmtree(folder_path_to_del, ignore_errors=True)
data = load_json(input_file)
used_enumerations.clear()
process_doclets(data, output_dir, editor_name)
shutil.rmtree(output_dir + 'tmp_json')
print('Done')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate documentation")
parser.add_argument(
"destination",
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default="../../../../../api.onlyoffice.com/site/docs/office-api/usage-api/" # Default value
)
args = parser.parse_args()
generate(args.destination)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -0,0 +1,245 @@
import os
import json
import re
import shutil
import argparse
import generate_docs_json
from datetime import datetime
# Configuration files
editors = [
"word",
"cell",
"slide",
"forms"
]
editors_names = {
"word": "Word",
"cell": "Spreadsheet",
"slide": "Presentation",
"forms": "Forms"
}
root = '../../../../..'
missing_examples = []
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def read_file_content(file_path):
try:
with open(file_path, encoding='utf-8') as f:
return f.read()
except Exception as e:
missing_examples.append(file_path)
# print(f"Failed to open file {file_path}: {e}")
return ""
def extract_js_comments_as_text(text):
# Extract single-line comments (after //)
single_line_comments = re.findall(r'^\s*//(.*)$', text, flags=re.MULTILINE)
# Extract multi-line comments (between /* and */)
multi_line_comments = re.findall(r'/\*(.*?)\*/', text, flags=re.DOTALL)
# Combine all found comments into a single list
all_comments = single_line_comments + multi_line_comments
# Join comments into a single text, separated by a space
return " ".join(comment.strip() for comment in all_comments if comment.strip())
def extract_examples_blocks(content: str):
blocks = []
current_block = {"comments": [], "code": []}
in_comment_section = True # Collect comments until code appears
current_comment_group = [] # Accumulate lines of the current comment
for line in content.splitlines():
stripped = line.strip()
if not stripped:
# Empty line
if in_comment_section and current_comment_group:
# Finish the current comment group
comment_text = " ".join(current_comment_group)
current_block["comments"].append(comment_text)
current_comment_group = []
elif not in_comment_section:
# Empty line in the code keep it as is
current_block["code"].append(line)
continue
if stripped.startswith("//"):
if in_comment_section:
# Remove comment marker and extra spaces
current_comment_group.append(extract_js_comments_as_text(stripped))
else:
# Comment after code starts finish the current block and start a new one
blocks.append({
"comments": current_block["comments"],
"code": "\n".join(current_block["code"]).rstrip()
})
current_block = {"comments": [], "code": []}
in_comment_section = True
# Start a new comment group with the current line
current_comment_group = [stripped[2:].strip()]
else:
# Code line
if in_comment_section:
if current_comment_group:
comment_text = " ".join(current_comment_group)
current_block["comments"].append(comment_text)
current_comment_group = []
in_comment_section = False
current_block["code"].append(line)
# Finalize any remaining comment group
if in_comment_section and current_comment_group:
comment_text = " ".join(current_comment_group)
current_block["comments"].append(comment_text)
# Save the last block if it's not empty
if current_block["comments"] or current_block["code"]:
blocks.append({
"comments": current_block["comments"],
"code": "\n".join(current_block["code"]).rstrip()
})
return blocks
def extract_examples_blocks_temp(content: str):
lines = content.splitlines()
comment_blocks = []
current_group = []
first_code_index = None
for i, line in enumerate(lines):
stripped = line.strip()
if not stripped:
if current_group:
comment_blocks.append(" ".join(current_group))
current_group = []
continue
if stripped.startswith("//"):
current_group.append(stripped[2:].strip())
else:
if current_group:
comment_blocks.append(" ".join(current_group))
current_group = []
first_code_index = i
break
code_part = ""
if first_code_index is not None:
code_part = "\n".join(lines[first_code_index:]).rstrip()
return [{"comments": comment_blocks, "code": code_part}]
def create_entry(system_message, user_message, assistant_message, model):
entry = {
"created_at": datetime.now().isoformat(" "),
"messages": [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message},
{"role": "assistant", "content": assistant_message}
],
"recommended": False,
"upvoted": True
}
if model is not "":
entry["model"] = model
return entry
def process_doclets(doclets, output_entries, editor_name, model):
system_message = f'You are an expert in API for library from OnlyOffice. The library provides functions for editing {editors_names[editor_name]} documents. Your functional capabilities: 1) Explanation of Onlyoffice JavaScript API classes and their methods and parameters. 2) Assistance in writing Onlyoffice JavaScript API examples upon user request. 3) Reviewing user examples, assisting in finding and fixing their mistakes.'
for doclet in doclets:
kind = doclet.get("kind", "").lower()
see = doclet.get("see", [])
# The "see" field must always be present
if not see:
continue
# Processing based on the "kind" value
if kind == "function":
method_name = doclet.get("name", "")
memberof = doclet.get("memberof", "")
# Functions must have both "name" (method_name) and "memberof" fields filled
if not (method_name and memberof):
continue
default_user_message = f"How do I use the method {method_name} of {memberof} class?"
elif kind == "class":
class_name = doclet.get("name", "")
default_user_message = f"How do I instantiate or work with the class {class_name}?"
elif kind == "typedef":
typedef_name = doclet.get("name", "")
default_user_message = f"How do I use the typedef {typedef_name}?"
else:
continue
# Read the content of the first file listed in the "see" field
content = read_file_content(f'{root}/{see[0]}')
if content == "":
continue
# now use only first block cause there is bad comments in examples
blocks = extract_examples_blocks_temp(content)
for block in blocks:
assistant_message = block['code']
# default entry
output_entries.append(create_entry(system_message, default_user_message, assistant_message, model))
# If the file content contains comments, create a separate entry for each one
for comment in block['comments']:
output_entries.append(create_entry(system_message, comment, assistant_message, model))
def generate(output_dir, model):
print('Generating documentation JSONL dataset...')
shutil.rmtree(output_dir, ignore_errors=True)
os.makedirs(output_dir)
generate_docs_json.generate(f'{output_dir}/tmp_json')
output_entries = []
output_filename = "dataset.jsonl"
for editor_name in editors:
input_file = os.path.join(f'{output_dir}/tmp_json', editor_name + ".json")
doclets = load_json(input_file)
process_doclets(doclets, output_entries, editor_name, model)
with open(f'{output_dir}/{output_filename}', "w", encoding="utf-8") as out_file:
for entry in output_entries:
out_file.write(json.dumps(entry, ensure_ascii=False) + "\n")
shutil.rmtree(f'{output_dir}/tmp_json')
print('Done')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate documentation JSONL dataset")
parser.add_argument(
"destination",
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default="../../../../../office-js-api/dataset" # Default value
)
parser.add_argument(
"model",
type=str,
help="Type of model",
nargs='?', # Indicates the argument is optional
default="" # Default value
)
args = parser.parse_args()
generate(args.destination, args.model)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -0,0 +1,85 @@
exports.handlers = {
processingComplete: function(e) {
const filteredDoclets = [];
function checkNullProps(oDoclet) {
for (let key of Object.keys(oDoclet)) {
if (oDoclet[key] == null) {
delete oDoclet[key];
}
if (typeof(oDoclet[key]) == "object") {
checkNullProps(oDoclet[key]);
}
}
}
for (let i = 0; i < e.doclets.length; i++) {
const doclet = e.doclets[i];
if (true == doclet.undocumented || doclet.kind == 'package') {
continue;
}
const filteredDoclet = {
comment: doclet.comment,
meta: doclet.meta ? {
lineno: doclet.meta.lineno,
columnno: doclet.meta.columnno
} : doclet.meta,
kind: doclet.kind,
since: doclet.since,
name: doclet.name,
type: doclet.type ? {
names: doclet.type.names,
parsedType: doclet.type.parsedType
} : doclet.type,
description: doclet.description,
memberof: doclet.memberof,
properties: doclet.properties ? doclet.properties.map(property => ({
type: property.type ? {
names: property.type.names,
parsedType: property.type.parsedType
} : property.type,
name: property.name,
description: property.description,
optional: property.optional,
defaultvalue: property.defaultvalue
})) : doclet.properties,
longname: doclet.longname,
scope: doclet.scope,
alias: doclet.alias,
params: doclet.params ? doclet.params.map(param => ({
type: param.type ? {
names: param.type.names,
parsedType: param.type.parsedType
} : param.type,
name: param.name,
description: param.description,
optional: param.optional,
defaultvalue: param.defaultvalue
})) : doclet.params,
returns: doclet.returns ? doclet.returns.map(returnObj => ({
type: {
names: returnObj.type.names,
parsedType: returnObj.type.parsedType
}
})) : doclet.returns,
see: doclet.see
};
checkNullProps(filteredDoclet)
filteredDoclets.push(filteredDoclet);
}
e.doclets.splice(0, e.doclets.length, ...filteredDoclets);
}
};

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/cell/plugin-events.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/common/base-plugin-events.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs-forms/plugin-events.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/slide/plugin-events.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/word/plugin-events.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/cell/api_plugins.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -1,8 +1,8 @@
{
"source": {
"include": ["../../../../sdkjs/common/plugins/plugin_base_api.js" ,"../../../../sdkjs/common/apiBase_plugins.js"]
"include": ["../../../../../sdkjs/common/plugins/plugin_base_api.js" ,"../../../../../sdkjs/common/apiBase_plugins.js"]
},
"plugins": ["./correct_doclets.js"],
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs-forms/apiPlugins.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -0,0 +1,16 @@
{
"source": {
"include": ["../../../../../sdkjs/slide/api_plugins.js"]
},
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -1,8 +1,8 @@
{
"source": {
"include": ["../../../../sdkjs/word/api_plugins.js", "../../../../sdkjs-forms/apiPlugins.js"]
"include": ["../../../../../sdkjs/word/api_plugins.js", "../../../../../sdkjs-forms/apiPlugins.js"]
},
"plugins": ["./correct_doclets.js"],
"plugins": ["../correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,

View File

@ -0,0 +1,111 @@
import os
import subprocess
import json
import argparse
import re
# Configuration files
configs = [
"./config/events/common.json",
"./config/events/word.json",
"./config/events/cell.json",
"./config/events/slide.json",
"./config/events/forms.json"
]
root = '../../../../..'
def generate(output_dir, md=False):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Generate JSON documentation
for config in configs:
editor_name = config.split('/')[-1].replace('.json', '')
output_file = os.path.join(output_dir, editor_name + ".json")
command = f"npx jsdoc -c {config} -X > {output_file}"
print(f"Generating {editor_name}.json: {command}")
subprocess.run(command, shell=True)
common_doclets_file = os.path.join(output_dir, 'common.json')
with open(common_doclets_file, 'r', encoding='utf-8') as f:
common_doclets_json = json.dumps(json.load(f))
os.remove(common_doclets_file)
# Append examples to JSON documentation
for config in configs:
if (config.find('common') != -1):
continue
editor_name = config.split('/')[-1].replace('.json', '')
example_folder_name = editor_name # name of folder with examples
output_file = os.path.join(output_dir, editor_name + ".json")
# Read the JSON file
with open(output_file, 'r', encoding='utf-8') as f:
data = json.load(f)
start_common_doclet_idx = len(data)
data += json.loads(common_doclets_json)
# Modify JSON data
for idx, doclet in enumerate(data):
if idx >= start_common_doclet_idx:
example_folder_name = 'common'
elif editor_name == 'forms':
example_folder_name = 'word'
if 'see' in doclet:
if doclet['see'] is not None:
doclet['see'][0] = doclet['see'][0].replace('{Editor}', example_folder_name.title())
file_path = f'{root}/' + doclet['see'][0]
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as see_file:
example_content = see_file.read()
# Extract the first line as a comment if it exists
lines = example_content.split('\n')
if lines[0].startswith('//'):
comment = lines[0] + '\n'
code_content = '\n'.join(lines[1:])
else:
comment = ''
code_content = example_content
doclet['examples'] = [remove_js_comments(comment) + code_content]
if md == False:
document_type = editor_name
if "forms" == document_type:
document_type = "pdf"
doclet['description'] = doclet['description'] + f'\n\n## Try it\n\n ```js document-builder={{"documentType": "{document_type}"}}\n{code_content}\n```'
# Write the modified JSON file back
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print("Documentation generation for plugin events completed.")
def remove_builder_lines(text):
lines = text.splitlines() # Split text into lines
filtered_lines = [line for line in lines if not line.strip().startswith("builder.")]
return "\n".join(filtered_lines)
def remove_js_comments(text):
# Remove single-line comments, leaving text after //
text = re.sub(r'^\s*//\s?', '', text, flags=re.MULTILINE)
# Remove multi-line comments, leaving text after /*
text = re.sub(r'/\*\s*|\s*\*/', '', text, flags=re.DOTALL)
return text.strip()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate documentation")
parser.add_argument(
"destination",
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default=f"{root}/office-js-api-declarations/office-js-api-plugins"
)
args = parser.parse_args()
generate(args.destination)

View File

@ -0,0 +1,380 @@
#!/usr/bin/env python3
import os
import json
import re
import shutil
import argparse
import generate_docs_events_json
# Папки для каждого editor_name
editors = {
"word": "text-document-api",
"cell": "spreadsheet-api",
"slide": "presentation-api",
"forms": "form-api"
}
missing_examples = []
used_enumerations = set()
def load_json(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def write_markdown_file(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
def remove_js_comments(text):
text = re.sub(r'^\s*//.*$', '', text, flags=re.MULTILINE)
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
return text.strip()
def correct_description(string, root='', isInTable=False):
"""
Cleans or transforms specific tags in the doclet description:
- <b> => ** (bold text)
- <note>...</note> => 💡 ...
- {@link ...} is replaced with a Markdown link
- If the description is missing, returns a default value.
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
if False == isInTable:
# Line breaks
string = string.replace('\r', '\\\n')
# Replace <b> tags with Markdown bold formatting
string = re.sub(r'<b>', '-**', string)
else:
string = re.sub(r'<b>', '**', string)
string = re.sub(r'</b>', '**', string)
# Replace <note> tags with an icon and text
string = re.sub(r'<note>(.*?)</note>', r'💡 \1', string, flags=re.DOTALL)
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
def process_link_tags(text, root=''):
"""
Finds patterns like {@link ...} and replaces them with Markdown links.
If the prefix 'global#' is found, a link to a typedef is generated,
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"
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)
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/Event_{typedef_name}.md)"
else:
# Handle links to class methods like ClassName#MethodName
try:
class_name, method_name = ref.split("#")
except ValueError:
return match.group(0)
display_text = label if label else ref # Keep the full notation, e.g., "Api#CreateSlide"
return f"[{display_text}]({root}{class_name}/Methods/{method_name}.md)"
return re.sub(r'{@link\s+([^}]+)}', replace_link, text)
def remove_line_breaks(s):
return re.sub(r'[\r\n]+', ' ', s)
def convert_jsdoc_array_to_ts(type_str):
p = re.compile(r'Array\.<([^>]+)>')
while True:
m = p.search(type_str)
if not m:
break
inner = convert_jsdoc_array_to_ts(m.group(1).strip())
type_str = type_str[:m.start()] + inner + '[]' + type_str[m.end():]
return type_str
def generate_data_types_markdown(types, enumerations, root=''):
converted = [convert_jsdoc_array_to_ts(t) for t in types]
primitives = {"string", "number", "boolean", "null", "undefined", "any", "object", "false", "true", "json", "function", "date", "{}"}
result = []
enum_names = {e['name'] for e in enumerations}
for t in converted:
base = t.rstrip('[]')
dims = t[len(base):]
if base in enum_names:
used_enumerations.add(base)
link = f"[Event_{base}]({root}../Enumeration/Event_{base}.md)"
elif base in primitives or re.match(r"^['\"].*['\"]$", base) or re.match(r"^-?\d+(\.\d+)?$", base):
link = base
else:
link = base
result.append(link + dims)
return " | ".join(result)
def escape_text_outside_code_blocks(md):
parts = re.split(r'(```.*?```)', md, flags=re.DOTALL)
for i in range(0, len(parts), 2):
parts[i] = parts[i].replace('<', '&lt;').replace('>', '&gt;')
return "".join(parts)
def generate_event_markdown(event, enumerations):
name = event['name']
desc = correct_description(event.get('description', ''))
params = event.get('params', [])
md = f"# {name}\n\n{desc}\n\n"
# Parameters
md += "## Parameters\n\n"
if params:
md += "| **Name** | **Data type** | **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))
md += f"| {p['name']} | {t_md} | {d} |\n"
md += "\n"
else:
md += "This event has no parameters.\n\n"
for ex in event.get('examples', []):
code = remove_js_comments(ex).strip()
md += f"```javascript\n{code}\n```\n\n"
return escape_text_outside_code_blocks(md)
def generate_enumeration_markdown(enumeration, enumerations):
"""
Generates Markdown documentation for a 'typedef' doclet.
This version only works with `enumeration['examples']` (an array of strings),
ignoring any single `enumeration['examples']` field.
"""
enum_name = enumeration['name']
if enum_name not in used_enumerations:
return None
description = enumeration.get('description', 'No description provided.')
description = correct_description(description, '../')
# Only use the 'examples' array
examples = enumeration.get('examples', [])
content = f"# Event_{enum_name}\n\n{description}\n\n"
parsed_type = enumeration['type'].get('parsedType')
if not parsed_type:
# If parsedType is missing, just list 'type.names' if available
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## Type\n\n"
t_md = generate_data_types_markdown(type_names, enumerations)
content += t_md + "\n\n"
else:
ptype = parsed_type['type']
# 1) Handle TypeUnion
if ptype == 'TypeUnion':
content += "## Type\n\nEnumeration\n\n"
content += "## Values\n\n"
for raw_t in enumeration['type']['names']:
# Attempt linking
if any(enum['name'] == raw_t for enum in enumerations):
used_enumerations.add(raw_t)
content += f"- [{raw_t}](../Enumeration/Event_{raw_t}.md)\n"
else:
content += f"- {raw_t}\n"
# 2) Handle TypeApplication (e.g. Object.<string, string>)
elif ptype == 'TypeApplication':
content += "## Type\n\nObject\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"
# 3) If properties are present, treat it like an object
if enumeration.get('properties') is not None:
content += generate_properties_markdown(enumeration['properties'], enumerations)
# 4) If it's neither TypeUnion nor TypeApplication, just output the type names
if ptype not in ('TypeUnion', 'TypeApplication'):
type_names = enumeration['type'].get('names', [])
if type_names:
content += "## 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"
else:
content += "\n\n## Example\n\n"
for i, ex_line in enumerate(examples, start=1):
# Remove JS comments
cleaned_example = remove_js_comments(ex_line).strip()
# Attempt splitting if the user used ```js
if '```js' in cleaned_example:
comment, code = cleaned_example.split('```js', 1)
comment = comment.strip()
code = code.strip()
if len(examples) > 1:
content += f"**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"
# No special fences, just show as code
content += f"```javascript\n{cleaned_example}\n```\n"
return escape_text_outside_code_blocks(content)
def generate_properties_markdown(properties, enumerations):
if properties is None:
return ''
content = "## Properties\n\n"
content += "| Name | Type | 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_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"
# Escape outside code blocks
return escape_text_outside_code_blocks(content)
def clean_editor_dir(editor_dir):
for root, dirs, files in os.walk(editor_dir, topdown=False):
for file in files:
if not file.endswith(('.json')):
os.remove(os.path.join(root, file))
for dir in dirs:
dir_path = os.path.join(root, dir)
# remove empty folder
if not os.listdir(dir_path):
os.rmdir(dir_path)
def clean_enum_files(editor_dir: str):
for root, _, files in os.walk(editor_dir, topdown=False):
for file in files:
if True == file.startswith('Event_') and False == file.endswith('.json'):
os.remove(os.path.join(root, file))
def process_events(data, editor_dir):
enumerations = []
events = []
for doclet in data:
kind = doclet.get('kind')
if kind == 'typedef':
enumerations.append(doclet)
elif kind == 'event':
events.append(doclet)
events_dir = f'{editor_dir}/Events'
clean_editor_dir(events_dir)
os.makedirs(events_dir, exist_ok=True)
used_enumerations.clear()
# пишем события
for ev in events:
path = os.path.join(events_dir, f"{ev['name']}.md")
write_markdown_file(path, generate_event_markdown(ev, enumerations))
if not ev.get('examples'):
missing_examples.append(os.path.relpath(path, events_dir))
# пишем перечисления, используемые событиями
enum_dir = os.path.join(editor_dir, 'Enumeration')
clean_enum_files(enum_dir)
os.makedirs(enum_dir, exist_ok=True)
prev = -1
while len(used_enumerations) != prev:
prev = len(used_enumerations)
for e in enumerations:
if e['name'] in used_enumerations:
generate_enumeration_markdown(e, enumerations)
for e in enumerations:
if e['name'] in used_enumerations:
path = os.path.join(enum_dir, f"Event_{e['name']}.md")
write_markdown_file(path, generate_enumeration_markdown(e, enumerations))
if not e.get('examples'):
missing_examples.append(os.path.relpath(path, editor_dir))
def generate_events(output_dir):
if output_dir.endswith('/'):
output_dir = output_dir[:-1]
tmp = os.path.join(output_dir, 'tmp_json')
generate_docs_events_json.generate(tmp, md=True)
for editor_name, folder in editors.items():
data = load_json(os.path.join(tmp, f"{editor_name}.json"))
process_events(data, os.path.join(output_dir, folder))
shutil.rmtree(tmp)
print("Done. Missing examples:", missing_examples)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate events documentation")
parser.add_argument(
"destination",
nargs="?",
default="../../../../../api.onlyoffice.com/site/docs/plugin-and-macros/interacting-with-editors/",
help="Output directory"
)
args = parser.parse_args()
generate_events(args.destination)

View File

@ -0,0 +1,111 @@
import os
import subprocess
import json
import argparse
import re
# Configuration files
configs = [
"./config/methods/common.json",
"./config/methods/word.json",
"./config/methods/cell.json",
"./config/methods/slide.json",
"./config/methods/forms.json"
]
root = '../../../../..'
def generate(output_dir, md=False):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Generate JSON documentation
for config in configs:
editor_name = config.split('/')[-1].replace('.json', '')
output_file = os.path.join(output_dir, editor_name + ".json")
command = f"npx jsdoc -c {config} -X > {output_file}"
print(f"Generating {editor_name}.json: {command}")
subprocess.run(command, shell=True)
common_doclets_file = os.path.join(output_dir, 'common.json')
with open(common_doclets_file, 'r', encoding='utf-8') as f:
common_doclets_json = json.dumps(json.load(f))
os.remove(common_doclets_file)
# Append examples to JSON documentation
for config in configs:
if (config.find('common') != -1):
continue
editor_name = config.split('/')[-1].replace('.json', '')
example_folder_name = editor_name # name of folder with examples
output_file = os.path.join(output_dir, editor_name + ".json")
# Read the JSON file
with open(output_file, 'r', encoding='utf-8') as f:
data = json.load(f)
start_common_doclet_idx = len(data)
data += json.loads(common_doclets_json)
# Modify JSON data
for idx, doclet in enumerate(data):
if idx >= start_common_doclet_idx:
example_folder_name = 'common'
elif editor_name == 'forms':
example_folder_name = 'word'
if 'see' in doclet:
if doclet['see'] is not None:
doclet['see'][0] = doclet['see'][0].replace('{Editor}', example_folder_name.title())
file_path = f'{root}/' + doclet['see'][0]
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as see_file:
example_content = see_file.read()
# Extract the first line as a comment if it exists
lines = example_content.split('\n')
if lines[0].startswith('//'):
comment = lines[0] + '\n'
code_content = '\n'.join(lines[1:])
else:
comment = ''
code_content = example_content
doclet['examples'] = [remove_js_comments(comment) + code_content]
if md == False:
document_type = editor_name
if "forms" == document_type:
document_type = "pdf"
doclet['description'] = doclet['description'] + f'\n\n## Try it\n\n ```js document-builder={{"documentType": "{document_type}"}}\n{code_content}\n```'
# Write the modified JSON file back
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print("Documentation generation for plugin methods completed.")
def remove_builder_lines(text):
lines = text.splitlines() # Split text into lines
filtered_lines = [line for line in lines if not line.strip().startswith("builder.")]
return "\n".join(filtered_lines)
def remove_js_comments(text):
# Remove single-line comments, leaving text after //
text = re.sub(r'^\s*//\s?', '', text, flags=re.MULTILINE)
# Remove multi-line comments, leaving text after /*
text = re.sub(r'/\*\s*|\s*\*/', '', text, flags=re.DOTALL)
return text.strip()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate documentation")
parser.add_argument(
"destination",
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default=f"{root}/office-js-api-declarations/office-js-api-plugins"
)
args = parser.parse_args()
generate(args.destination)

View File

@ -3,17 +3,20 @@ import json
import re
import shutil
import argparse
import generate_docs_plugins_json
import generate_docs_methods_json
# Configuration files
editors = [
"word",
"cell",
"slide",
"forms"
]
editors = {
"word": "text-document-api",
"cell": "spreadsheet-api",
"slide": "presentation-api",
"forms": "form-api"
}
missing_examples = []
used_enumerations = set()
cur_editor_name = None
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
@ -28,25 +31,86 @@ def remove_js_comments(text):
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # multi-line
return text.strip()
def correct_description(string):
def process_link_tags(text, root=''):
"""
Cleans up or transforms certain tags in a doclet description:
- <b> => **
Finds patterns like {@link ...} and replaces them with Markdown links.
If the prefix 'global#' is found, a link to a typedef is generated,
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"
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)
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:
class_name, method_name = ref.split("#")
except ValueError:
return match.group(0)
display_text = label if label else ref # Keep the full notation, e.g., "Api#CreateSlide"
return f"[{display_text}]({root}{class_name}/Methods/{method_name}.md)"
return re.sub(r'{@link\s+([^}]+)}', replace_link, text)
def correct_description(string, root='', isInTable=False):
"""
Cleans or transforms specific tags in the doclet description:
- <b> => ** (bold text)
- <note>...</note> => 💡 ...
- Provide a default if None.
- {@link ...} is replaced with a Markdown link
- If the description is missing, returns a default value.
- All '\r' characters are replaced with '\n'.
"""
if string is None:
return 'No description provided.'
if False == isInTable:
# Line breaks
string = string.replace('\r', '\\\n')
# Replace <b> tags with Markdown bold formatting
string = re.sub(r'<b>', '-**', string)
else:
string = re.sub(r'<b>', '**', string)
# Replace <b> tags with markdown bold
string = re.sub(r'<b>', '**', string)
string = re.sub(r'</b>', '**', string)
# Convert <note>...</note> to a little icon + text
# Replace <note> tags with an icon and text
string = re.sub(r'<note>(.*?)</note>', r'💡 \1', string, flags=re.DOTALL)
# Process {@link ...} constructions
string = process_link_tags(string, root)
return string
def correct_default_value(value, enumerations, classes):
if value is None:
if value is None or value == '':
return ''
if value == True:
@ -98,15 +162,24 @@ def escape_text_outside_code_blocks(markdown: str) -> str:
# Even indices (0, 2, 4, ...) are outside code blocks,
# odd indices (1, 3, 5, ...) are actual code blocks.
for i in range(0, len(parts), 2):
# Only escape in parts outside code blocks
parts[i] = (parts[i]
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('{', '&#123;')
.replace('}', '&#125;')
)
text = (parts[i]
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('{', '&#123;')
.replace('}', '&#125;'))
parts[i] = escape_brackets_in_quotes(text)
return "".join(parts)
def escape_brackets_in_quotes(text: str) -> str:
return re.sub(
r"(['\"])(.*?)(?<!\\)\1",
lambda m: m.group(1)
+ m.group(2).replace('[', r'\[').replace(']', r'\]')
+ m.group(1),
text
)
def get_base_type(ts_type: str) -> str:
"""
Given a TypeScript-like type (e.g. "Drawing[][]"), return the
@ -118,64 +191,147 @@ def get_base_type(ts_type: str) -> str:
ts_type = ts_type[:-2]
return ts_type
def generate_data_types_markdown(types, enumerations, classes, root='../../'):
def generate_data_types_markdown(types, enumerations, classes, root='../'):
"""
1) Convert each raw JSDoc type from Array.<T> to T[].
2) Split union types if needed (usually they're provided as separate
elements in 'types' already, but let's be safe).
3) For each type, extract the base type (e.g. "Drawing" from "Drawing[]").
4) If the base type matches an enumeration or class, link the entire
T[]-based string.
5) Join with " | ".
1) Converts each type from JSDoc (e.g., Array.<T>) to T[].
2) Processes union types by splitting them using '|'.
3) Supports multidimensional arrays, e.g., (string|ApiRange|number)[].
4) If the base type matches the name of an enumeration or class, generates a link.
5) The final types are joined using " | ".
"""
# Convert each type from JSDoc format to TypeScript format (e.g., T[])
converted = [convert_jsdoc_array_to_ts(t) for t in types]
# Convert each raw type from JSDoc to TS
converted = [convert_jsdoc_array_to_ts(t) for t in types] # e.g. ["Drawing[]", "Foo[]", ...]
# Set of primitive types
primitive_types = {"string", "number", "boolean", "null", "undefined", "any", "object", "false", "true", "json", "function", "date", "{}"}
def is_primitive(type):
if (type.lower() in primitive_types or
(type.startswith('"') and type.endswith('"')) or
(type.startswith("'") and type.endswith("'")) or
type.replace('.', '', 1).isdigit() or
(type.startswith('-') and type[1:].replace('.', '', 1).isdigit())):
return True
return False
# For each converted type (like "Drawing[]"), see if the base is in enumerations or classes
def link_if_known(ts_type):
base = get_base_type(ts_type) # e.g. "Drawing" from "Drawing[]"
ts_type = ts_type.strip()
# Count the number of array dimensions, e.g., "[][]" has 2 dimensions
array_dims = 0
while ts_type.endswith("[]"):
array_dims += 1
ts_type = ts_type[:-2].strip()
# Check enumerations first
for enum in enumerations:
if enum['name'] == base:
# Replace the entire token with a link
return f"[{ts_type}]({root}Enumeration/{base}.md)"
# Process generic types, e.g., Object.<string, editorType>
if ".<" in ts_type and ts_type.endswith(">"):
import re
m = re.match(r'^(.*?)\.<(.*)>$', ts_type)
if m:
base_part = m.group(1).strip()
generic_args_str = m.group(2).strip()
# Process the base part of the type
found = False
for enum in enumerations:
if enum['name'] == base_part:
used_enumerations.add(base_part)
base_result = f"[{base_part}]({root}Enumeration/{base_part}.md)"
found = True
break
if not found:
if base_part in classes:
base_result = f"[{base_part}]({root}{base_part}/{base_part}.md)"
elif is_primitive(base_part):
base_result = base_part
elif cur_editor_name == "forms":
base_result = f"[{base_part}]({root}../text-document-api/{base_part}/{base_part}.md)"
else:
print(f"Unknown type encountered: {base_part}")
base_result = base_part
# Split the generic parameters by commas and process each recursively
generic_args = [link_if_known(x) for x in generic_args_str.split(",")]
result = base_result + ".&lt;" + ", ".join(generic_args) + "&gt;"
result += "[]" * array_dims
return result
# Check classes
if base in classes:
return f"[{ts_type}]({root}{base}/{base}.md)"
# Process union types: if the type is enclosed in parentheses
if ts_type.startswith("(") and ts_type.endswith(")"):
inner = ts_type[1:-1].strip()
subtypes = [sub.strip() for sub in inner.split("|")]
if len(subtypes) == 1:
result = link_if_known(subtypes[0])
else:
processed = [link_if_known(subtype) for subtype in subtypes]
result = "(" + " | ".join(processed) + ")"
result += "[]" * array_dims
return result
# Otherwise just return as-is
return ts_type
# If not a generic or union type process the base type
else:
base = ts_type
found = False
for enum in enumerations:
if enum['name'] == base:
used_enumerations.add(base)
result = f"[{base}]({root}Enumeration/{base}.md)"
found = True
break
if not found:
if base in classes:
result = f"[{base}]({root}{base}/{base}.md)"
elif is_primitive(base):
result = base
elif cur_editor_name == "forms":
result = f"[{base}]({root}../text-document-api/{base}/{base}.md)"
else:
print(f"Unknown type encountered: {base}")
result = base
result += "[]" * array_dims
return result
# Build final list of possibly-linked types
# Apply link_if_known to each converted type
linked = [link_if_known(ts_t) for ts_t in converted]
# Join them with " | "
param_types_md = ' | '.join(linked)
# Join results using " | "
param_types_md = r' | '.join(linked)
param_types_md = param_types_md.replace("|", r"\|")
# If there's still leftover angle brackets for generics, gently escape or link them
# e.g. "Object.<string, number>" => "Object.&lt;string, number&gt;"
# or do more specialized linking if you want to handle them deeper.
# Escape remaining angle brackets for generics
def replace_leftover_generics(match):
element = match.group(1).strip()
return f"&lt;{element}&gt;"
param_types_md = re.sub(r'<([^<>]+)>', replace_leftover_generics, param_types_md)
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 += generate_properties_markdown(properties, enumerations, classes)
content += "## Methods\n\n"
for method in methods:
content += "\n## Methods\n\n"
content += "| Method | Returns | Description |\n"
content += "| ------ | ------- | ----------- |\n"
for method in sorted(methods, key=lambda m: m['name']):
method_name = method['name']
content += f"- [{method_name}](./Methods/{method_name}.md)\n"
# Escape just before returning
# Get the type of return values
returns = method.get('returns', [])
if returns:
return_type_list = returns[0].get('type', {}).get('names', [])
returns_markdown = generate_data_types_markdown(return_type_list, enumerations, classes, '../')
else:
returns_markdown = "None"
# Processing the method description
description = remove_line_breaks(correct_description(method.get('description', 'No description provided.'), '../', True))
# Form a link to the method document
method_link = f"[{method_name}](./{method_name}.md)"
content += f"| {method_link} | {returns_markdown} | {description} |\n"
return escape_text_outside_code_blocks(content)
def generate_method_markdown(method, enumerations, classes):
@ -186,7 +342,7 @@ def generate_method_markdown(method, enumerations, classes):
method_name = method['name']
description = method.get('description', 'No description provided.')
description = correct_description(description)
description = correct_description(description, '../')
params = method.get('params', [])
returns = method.get('returns', [])
memberof = method.get('memberof', '')
@ -197,10 +353,10 @@ def generate_method_markdown(method, enumerations, classes):
content = f"# {method_name}\n\n{description}\n\n"
# Syntax
param_list = ', '.join([param['name'] for param in params]) if params else ''
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"
if memberof:
content += f"`expression` - A variable that represents a [{memberof}](../{memberof}.md) class.\n\n"
content += f"`expression` - A variable that represents a [{memberof}](Methods.md) class.\n\n"
# Parameters
content += "## Parameters\n\n"
@ -211,7 +367,7 @@ def generate_method_markdown(method, enumerations, classes):
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.')))
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_default = correct_default_value(param.get('defaultvalue', ''), enumerations, classes)
@ -264,10 +420,10 @@ def generate_properties_markdown(properties, enumerations, classes, root='../'):
content += "| Name | Type | Description |\n"
content += "| ---- | ---- | ----------- |\n"
for prop in properties:
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))
prop_description = remove_line_breaks(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, classes, root)
content += f"| {prop_name} | {param_types_md} | {prop_description} |\n"
@ -281,10 +437,13 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
This version only works with `enumeration['examples']` (an array of strings),
ignoring any single `enumeration['examples']` field.
"""
enum_name = enumeration['name']
if enum_name not in used_enumerations:
return None
description = enumeration.get('description', 'No description provided.')
description = correct_description(description)
description = correct_description(description, '../')
# Only use the 'examples' array
examples = enumeration.get('examples', [])
@ -309,6 +468,7 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
for raw_t in enumeration['type']['names']:
# Attempt linking
if any(enum['name'] == raw_t for enum in enumerations):
used_enumerations.add(raw_t)
content += f"- [{raw_t}](../Enumeration/{raw_t}.md)\n"
elif raw_t in classes:
content += f"- [{raw_t}](../{raw_t}/{raw_t}.md)\n"
@ -363,55 +523,86 @@ def generate_enumeration_markdown(enumeration, enumerations, classes):
return escape_text_outside_code_blocks(content)
def clean_methods_dir(methods_dir):
for root, dirs, files in os.walk(methods_dir, topdown=False):
for file in files:
if not file.endswith(('.json')):
os.remove(os.path.join(root, file))
for dir in dirs:
dir_path = os.path.join(root, dir)
# remove empty folder
if not os.listdir(dir_path):
os.rmdir(dir_path)
def clean_enum_files(editor_dir: str):
for root, _, files in os.walk(editor_dir, topdown=False):
for file in files:
if False == file.startswith('Event_') and False == file.endswith('.json'):
os.remove(os.path.join(root, file))
def process_doclets(data, output_dir, editor_name):
global cur_editor_name
cur_editor_name = editor_name
classes = {}
classes_props = {}
enumerations = []
editor_dir = os.path.join(output_dir, editor_name)
editor_dir = os.path.join(output_dir, editors[editor_name])
methods_dir = os.path.join(output_dir, editors[editor_name], 'Methods')
clean_methods_dir(methods_dir)
os.makedirs(methods_dir, exist_ok=True)
for doclet in data:
if doclet['kind'] == 'class':
class_name = doclet['name']
classes[class_name] = []
classes_props[class_name] = doclet.get('properties', None)
if class_name:
if class_name not in classes:
classes[class_name] = []
classes_props[class_name] = doclet.get('properties', None)
elif doclet['kind'] == 'function':
class_name = doclet.get('memberof')
if class_name:
if class_name not in classes:
classes[class_name] = []
classes[class_name] = []
classes[class_name].append(doclet)
elif doclet['kind'] == 'typedef':
enumerations.append(doclet)
# Process classes
for class_name, methods in classes.items():
class_dir = os.path.join(editor_dir, class_name)
methods_dir = os.path.join(class_dir, 'Methods')
os.makedirs(methods_dir, exist_ok=True)
# Process api methods
class_name = 'Api'
methods = classes[class_name]
# Write class file
class_content = generate_class_markdown(
class_name,
methods,
classes_props[class_name],
enumerations,
classes
)
write_markdown_file(os.path.join(methods_dir, f"Methods.md"), class_content)
# Write class file
class_content = generate_class_markdown(
class_name,
methods,
classes_props[class_name],
enumerations,
classes
)
write_markdown_file(os.path.join(class_dir, f"{class_name}.md"), class_content)
# Write method files
for method in methods:
method_file_path = os.path.join(methods_dir, f"{method['name']}.md")
method_content = generate_method_markdown(method, enumerations, classes)
write_markdown_file(method_file_path, method_content)
# Write method files
for method in methods:
method_file_path = os.path.join(methods_dir, f"{method['name']}.md")
method_content = generate_method_markdown(method, enumerations, classes)
write_markdown_file(method_file_path, method_content)
if not method.get('examples', ''):
missing_examples.append(os.path.relpath(method_file_path, output_dir))
if not method.get('examples', ''):
missing_examples.append(os.path.relpath(method_file_path, output_dir))
# Process enumerations
enum_dir = os.path.join(editor_dir, 'Enumeration')
clean_enum_files(enum_dir)
os.makedirs(enum_dir, exist_ok=True)
# idle run
prev_used_count = -1
while len(used_enumerations) != prev_used_count:
prev_used_count = len(used_enumerations)
for enum in [e for e in enumerations if e['name'] in used_enumerations]:
enum_content = generate_enumeration_markdown(enum, enumerations, classes)
for enum in enumerations:
enum_file_path = os.path.join(enum_dir, f"{enum['name']}.md")
enum_content = generate_enumeration_markdown(enum, enumerations, classes)
@ -428,15 +619,13 @@ def generate(output_dir):
if output_dir[-1] == '/':
output_dir = output_dir[:-1]
generate_docs_plugins_json.generate(output_dir + '/tmp_json', md=True)
for editor_name in editors:
generate_docs_methods_json.generate(output_dir + '/tmp_json', md=True)
for editor_name, folder_name in editors.items():
input_file = os.path.join(output_dir + '/tmp_json', editor_name + ".json")
shutil.rmtree(output_dir + f'/{editor_name.title()}', ignore_errors=True)
os.makedirs(output_dir + f'/{editor_name.title()}')
data = load_json(input_file)
process_doclets(data, output_dir, editor_name.title())
used_enumerations.clear()
process_doclets(data, output_dir, editor_name)
shutil.rmtree(output_dir + '/tmp_json')
print('Done')
@ -448,7 +637,7 @@ if __name__ == "__main__":
type=str,
help="Destination directory for the generated documentation",
nargs='?', # Indicates the argument is optional
default="../../../../office-js-api/Plugins/" # Default value
default="../../../../../api.onlyoffice.com/site/docs/plugin-and-macros/interacting-with-editors/" # Default value
)
args = parser.parse_args()
generate(args.destination)

View File

@ -97,7 +97,7 @@ def get_projects(pro_json_path, platform):
records_src = data[module]
records = get_full_projects_list(data, records_src)
print(records)
#print(records)
for rec in records:
params = []
@ -133,7 +133,7 @@ def get_projects(pro_json_path, platform):
is_needed_platform_exist = False
for pl in platform_records:
if is_exist_in_array(params, pl):
is_needed_platform_exist = True;
is_needed_platform_exist = True
break
# if one config exists => all needed must exists
@ -144,7 +144,7 @@ def get_projects(pro_json_path, platform):
if is_exist_in_array(platform_records, item):
continue
is_needed_config_exist = True
break;
break
if is_needed_platform_exist:
if not is_exist_in_array(params, platform):
@ -164,6 +164,18 @@ def get_projects(pro_json_path, platform):
if is_append:
result.append(root_dir + record)
# delete duplicates
old_results = result
result = []
map_results = set()
for item in old_results:
proj = item.replace("\\", "/")
if proj in map_results:
continue
map_results.add(proj)
result.append(proj)
if is_log:
print(result)
return result

View File

@ -19,6 +19,7 @@
"core/PdfFile/PdfFile.pro",
"core/DjVuFile/DjVuFile.pro",
"core/XpsFile/XpsFile.pro",
"core/OFDFile/OFDFile.pro",
"core/HtmlFile2/HtmlFile2.pro",
"core/Fb2File/Fb2File.pro",
"core/EpubFile/CEpubFile.pro",
@ -93,7 +94,7 @@
"[win]desktop-apps/win-linux/extras/projicons/ProjIcons.pro",
"[win,!win_xp]desktop-apps/win-linux/extras/update-daemon/UpdateDaemon.pro",
"[win,!win_xp]desktop-apps/win-linux/extras/online-installer/OnlineInstaller.pro"
"[win_xp]desktop-apps/win-linux/extras/online-installer/OnlineInstaller.pro"
],
"mobile" : [

View File

@ -68,6 +68,7 @@ AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_HTMLR = AVS_OFFICESTUDIO_FILE_CROS
AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_HTMLRMenu = AVS_OFFICESTUDIO_FILE_CROSSPLATFORM + 0x0007
AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_HTMLRCanvas = AVS_OFFICESTUDIO_FILE_CROSSPLATFORM + 0x0008
AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_PDFA = AVS_OFFICESTUDIO_FILE_CROSSPLATFORM + 0x0009
AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_OFD = AVS_OFFICESTUDIO_FILE_CROSSPLATFORM + 0x000a
AVS_OFFICESTUDIO_FILE_IMAGE = 0x0400
AVS_OFFICESTUDIO_FILE_IMAGE_JPG = AVS_OFFICESTUDIO_FILE_IMAGE + 0x0001

View File

@ -67,7 +67,7 @@ def install_deps():
print("OK")
base.cmd("sudo", ["apt-get", "-y", "install", "npm", "yarn"], True)
base.cmd("sudo", ["npm", "install", "-g", "grunt-cli"])
base.cmd("sudo", ["npm", "install", "-g", "pkg"])
base.cmd("sudo", ["npm", "install", "-g", "@yao-pkg/pkg"])
# java
java_error = base.cmd("sudo", ["apt-get", "-y", "install", "openjdk-11-jdk"], True)

View File

@ -1 +1 @@
8.2.2
9.0.4