Compare commits

..

1 Commits

Author SHA1 Message Date
6736f84d54 Fixed jsdoc md generation 2024-08-02 21:26:44 +07:00
66 changed files with 698 additions and 3572 deletions

View File

@ -11,4 +11,5 @@ RUN rm /usr/bin/python && ln -s /usr/bin/python2 /usr/bin/python
ADD . /build_tools
WORKDIR /build_tools
CMD ["sh", "-c", "cd tools/linux && python3 ./automate.py"]
CMD cd tools/linux && \
python3 ./automate.py

View File

@ -27,7 +27,7 @@ parser.add_option("--db-pass", action="store", type="string", dest="db-pass", de
parser.add_option("--compiler", action="store", type="string", dest="compiler", default="", help="defines compiler name. It is not recommended to use it as it's defined automatically (msvc2015, msvc2015_64, gcc, gcc_64, clang, clang_64, etc)")
parser.add_option("--no-apps", action="store", type="string", dest="no-apps", default="0", help="disables building desktop apps that use qt")
parser.add_option("--themesparams", action="store", type="string", dest="themesparams", default="", help="provides settings for generating presentation themes thumbnails")
parser.add_option("--git-protocol", action="store", type="string", dest="git-protocol", default="auto", help="can be used only if update is set to true - 'https', 'ssh'")
parser.add_option("--git-protocol", action="store", type="string", dest="git-protocol", default="https", help="can be used only if update is set to true - 'https', 'ssh'")
parser.add_option("--branding", action="store", type="string", dest="branding", default="", help="provides branding path")
parser.add_option("--branding-name", action="store", type="string", dest="branding-name", default="", help="provides branding name")
parser.add_option("--branding-url", action="store", type="string", dest="branding-url", default="", help="provides branding url")

View File

@ -92,8 +92,6 @@ if config.check_option("module", "desktop"):
config.extend_option("config", "updmodule")
base.set_env("DESKTOP_URL_UPDATES_MAIN_CHANNEL", "https://download.onlyoffice.com/install/desktop/editors/windows/onlyoffice/appcast.json")
base.set_env("DESKTOP_URL_UPDATES_DEV_CHANNEL", "https://download.onlyoffice.com/install/desktop/editors/windows/onlyoffice/appcastdev.json")
base.set_env("DESKTOP_URL_INSTALL_CHANNEL", "https://download.onlyoffice.com/install/desktop/editors/windows/distrib/onlyoffice/<file>")
base.set_env("DESKTOP_URL_INSTALL_DEV_CHANNEL", "https://download.onlyoffice.com/install/desktop/editors/windows/onlyoffice/onlineinstallerdev/<file>")
# build
build_sln.make()

View File

@ -10,17 +10,15 @@ import package_utils as utils
# parse
parser = argparse.ArgumentParser(description="Build packages.")
parser.add_argument("-P", "--platform", dest="platform", type=str,
action="store", help="Defines platform", required=True)
parser.add_argument("-T", "--targets", dest="targets", type=str, nargs="+",
action="store", help="Defines targets", required=True)
parser.add_argument("-V", "--version", dest="version", type=str,
action="store", help="Defines version")
parser.add_argument("-B", "--build", dest="build", type=str,
action="store", help="Defines build")
parser.add_argument("-H", "--branch", dest="branch", type=str,
action="store", help="Defines branch")
action="store", help="Defines platform", required=True)
parser.add_argument("-T", "--targets", dest="targets", type=str, nargs="+",
action="store", help="Defines targets", required=True)
parser.add_argument("-R", "--branding", dest="branding", type=str,
action="store", help="Provides branding path")
action="store", help="Provides branding path")
parser.add_argument("-V", "--version", dest="version", type=str,
action="store", help="Defines version")
parser.add_argument("-B", "--build", dest="build", type=str,
action="store", help="Defines build")
args = parser.parse_args()
# vars
@ -31,20 +29,13 @@ common.targets = args.targets
common.clean = "clean" in args.targets
common.sign = "sign" in args.targets
common.deploy = "deploy" in args.targets
if args.version: common.version = args.version
else: common.version = utils.get_env("PRODUCT_VERSION", "0.0.0")
utils.set_env("PRODUCT_VERSION", common.version)
utils.set_env("BUILD_VERSION", common.version)
if args.build: common.build = args.build
else: common.build = utils.get_env("BUILD_NUMBER", "0")
utils.set_env("BUILD_NUMBER", common.build)
if args.branch: common.branch = args.branch
else: common.branch = utils.get_env("BRANCH_NAME", "null")
utils.set_env("BRANCH_NAME", common.branch)
common.version = args.version if args.version else utils.get_env("BUILD_VERSION", "0.0.0")
common.build = args.build if args.build else utils.get_env("BUILD_NUMBER", "0")
common.branding = args.branding
common.timestamp = utils.get_timestamp()
common.workspace_dir = utils.get_abspath(utils.get_script_dir(__file__) + "/..")
common.branding_dir = utils.get_abspath(common.workspace_dir + "/" + args.branding) if args.branding else common.workspace_dir
common.deploy_data = utils.get_path(common.workspace_dir + "/deploy.txt")
common.summary = []
utils.log("os_family: " + common.os_family)
utils.log("platform: " + str(common.platform))
@ -73,14 +64,15 @@ import package_mobile
# build
utils.set_cwd(common.workspace_dir, verbose=True)
utils.delete_file(common.deploy_data)
if "core" in common.targets:
package_core.make()
if "closuremaps_sdkjs_opensource" in common.targets:
if "closuremaps_opensource" in common.targets:
package_core.deploy_closuremaps_sdkjs("opensource")
if "closuremaps_sdkjs_commercial" in common.targets:
package_core.deploy_closuremaps_sdkjs("commercial")
if "closuremaps_webapps" in common.targets:
package_core.deploy_closuremaps_webapps("opensource")
if "closuremaps_commercial" in common.targets:
package_core.deploy_closuremaps_sdkjs("commercial")
package_core.deploy_closuremaps_webapps("commercial")
if "desktop" in common.targets:
package_desktop.make()
if "builder" in common.targets:
@ -91,8 +83,6 @@ if "server_enterprise" in common.targets:
package_server.make("enterprise")
if "server_developer" in common.targets:
package_server.make("developer")
if "server_prerequisites" in common.targets:
package_server.make("prerequisites")
if "mobile" in common.targets:
package_mobile.make()

View File

@ -39,9 +39,6 @@ def is_os_arm():
return False
return True
def get_platform():
return platform.machine().lower()
def is_python_64bit():
return (struct.calcsize("P") == 8)
@ -175,13 +172,6 @@ def find_file(path, pattern):
for filename in fnmatch.filter(filenames, pattern):
return os.path.join(root, filename)
def find_files(path, pattern):
result = []
for root, dirnames, filenames in os.walk(path):
for filename in fnmatch.filter(filenames, pattern):
result.append(os.path.join(root, filename))
return result
def create_dir(path):
path2 = get_path(path)
if not os.path.exists(path2):
@ -377,7 +367,7 @@ def cmd(prog, args=[], is_no_errors=False):
else:
command = prog
for arg in args:
command += (" \"" + arg.replace('\"', '\\\"') + "\"")
command += (" \"" + arg + "\"")
ret = subprocess.call(command, stderr=subprocess.STDOUT, shell=True)
if ret != 0 and True != is_no_errors:
sys.exit("Error (" + prog + "): " + str(ret))
@ -413,7 +403,7 @@ def cmd_exe(prog, args, is_no_errors=False):
else:
command = prog
for arg in args:
command += (" \"" + arg.replace('\"', '\\\"') + "\"")
command += (" \"" + arg + "\"")
process = subprocess.Popen(command, stderr=subprocess.STDOUT, shell=True, env=env_dir)
ret = process.wait()
if ret != 0 and True != is_no_errors:
@ -428,13 +418,6 @@ 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)
@ -443,13 +426,12 @@ def cmd_and_return_cwd(prog, args=[], is_no_errors=False):
def run_command(sCommand):
popen = subprocess.Popen(sCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
result = {'stdout' : '', 'stderr' : '', 'returncode' : 0}
result = {'stdout' : '', 'stderr' : ''}
try:
stdout, stderr = popen.communicate()
popen.wait()
result['stdout'] = stdout.strip().decode('utf-8', errors='ignore')
result['stderr'] = stderr.strip().decode('utf-8', errors='ignore')
result['returncode'] = popen.returncode
finally:
popen.stdout.close()
popen.stderr.close()
@ -514,37 +496,12 @@ def set_cwd(dir):
return
# git ---------------------------------------------------
def git_get_origin():
cur_dir = os.getcwd()
os.chdir(get_script_dir() + "/../")
ret = run_command("git config --get remote.origin.url")["stdout"]
os.chdir(cur_dir)
return ret
def git_is_ssh():
git_protocol = config.option("git-protocol")
if (git_protocol == "https"):
return False
if (git_protocol == "ssh"):
return True
origin = git_get_origin()
if (git_protocol == "auto") and (origin.find(":ONLYOFFICE/") != -1):
return True
return False
def get_ssh_base_url():
cur_origin = git_get_origin()
ind = cur_origin.find(":ONLYOFFICE/")
if (ind == -1):
return "git@github.com:ONLYOFFICE/"
return cur_origin[:ind+12]
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"
if git_is_ssh():
url = get_ssh_base_url() + repo + ".git"
if config.option("git-protocol") == "ssh":
url = "git@github.com:ONLYOFFICE/" + repo + ".git"
folder = get_script_dir() + "/../../" + repo
if is_current_dir:
folder = repo
@ -577,12 +534,10 @@ def get_repositories():
result.update(get_sdkjs_addons())
result["onlyoffice.github.io"] = [False, False]
result["web-apps"] = [False, False]
result.update(get_web_apps_addons())
result["dictionaries"] = [False, False]
result["core-fonts"] = [False, False]
if config.check_option("module", "server"):
result.update(get_web_apps_addons())
if config.check_option("module", "builder"):
result["document-templates"] = [False, False]
@ -615,8 +570,8 @@ 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"
if git_is_ssh():
url = get_ssh_base_url() + repo + ".git"
if config.option("git-protocol") == "ssh":
url = "git@github.com:ONLYOFFICE/" + repo + ".git"
folder = get_script_dir() + "/../../" + repo
if is_current_dir:
folder = repo
@ -783,9 +738,8 @@ def qt_config(platform):
if (-1 != platform.find("xp")):
config_param += " build_xp"
if ("ios" == platform):
if (config.check_option("bitcode", "yes")):
set_env("BITCODE_GENERATION_MODE", "bitcode")
set_env("ENABLE_BITCODE", "YES")
set_env("BITCODE_GENERATION_MODE", "bitcode")
set_env("ENABLE_BITCODE", "YES")
config_param = config_param.replace("desktop", "")
config_param += " iphoneos device"
if (-1 == config_param_lower.find("debug")):
@ -1233,19 +1187,18 @@ 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"])
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("HtmlRenderer", ["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("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", "HtmlRenderer", "XpsFile", "DjVuFile", "HtmlFile2", "Fb2File", "EpubFile", "doctrenderer", "DocxRenderer"])
if is_file("./allfontsgen"):
cmd("chmod", ["-v", "+x", "./allfontsgen"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./allfontsgen"], True)
@ -1253,7 +1206,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"])
if is_file("./pluginsmanager"):
cmd("chmod", ["-v", "+x", "./pluginsmanager"])
cmd("install_name_tool", ["-add_rpath", "@executable_path", "./pluginsmanager"], True)
@ -1270,14 +1223,9 @@ 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"])
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"])
mac_correct_rpath_binary("./docbuilder", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "PdfFile", "HtmlRenderer", "XpsFile", "DjVuFile", "HtmlFile2", "Fb2File", "EpubFile", "doctrenderer", "DocxRenderer"])
mac_correct_rpath_library("docbuilder.c", ["icudata.58", "icuuc.58", "UnicodeConverter", "kernel", "kernel_network", "graphics", "doctrenderer"])
cmd("install_name_tool", ["-add_rpath", "@loader_path", "libdocbuilder.c.dylib"], True)
os.chdir(cur_dir)
return
@ -1287,9 +1235,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", "HtmlRenderer", "XpsFile", "DjVuFile", "hunspell", "ooxmlsignature"])
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", "HtmlRenderer", "XpsFile", "DjVuFile", "hunspell", "ooxmlsignature"])
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"])
@ -1323,7 +1271,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.c.so", "icuuc.so.58", "doctrenderer.so", "graphics.so", "kernel.so", "kernel_network.so", "UnicodeConverter.so"])
return
def common_check_version(name, good_version, clean_func):
@ -1379,7 +1327,7 @@ def copy_marketplace_plugin(dst_dir, is_name_as_guid=False, is_desktop_local=Fal
git_dir = __file__script__path__ + "/../.."
if False:
# old version
copy_sdkjs_plugin(git_dir + "/desktop-sdk/ChromiumBasedEditors/plugins", dst_dir, "manager", is_name_as_guid, is_desktop_local)
base.copy_sdkjs_plugin(git_dir + "/desktop-sdk/ChromiumBasedEditors/plugins", dst_dir, "manager", is_name_as_guid, is_desktop_local)
return
src_dir_path = git_dir + "/onlyoffice.github.io/store/plugin"
name = "marketplace"
@ -1445,7 +1393,6 @@ def support_old_versions_plugins(out_dir):
def generate_sdkjs_plugin_list(dst):
plugins_list = config.option("sdkjs-plugin").rsplit(", ") \
+ config.option("sdkjs-plugin-server").rsplit(", ")
plugins_list = list(filter(None, plugins_list))
with open(get_path(dst), 'w') as file:
dump = json.dumps(sorted(plugins_list), indent=4)
file.write(re.sub(r"^(\s{4})", '\t', dump, 0, re.MULTILINE))
@ -1807,50 +1754,3 @@ def apply_patch(file, patch):
#file_content_new = "\n#if 0" + file_content_old + "#else" + file_content_new + "#endif\n"
replaceInFile(file, file_content_old, file_content_new)
return
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
if ("windows" == host_platform()):
download_platform = "win-"
elif ("linux" == host_platform()):
download_platform = "linux-"
else:
download_platform = "mac-"
download_platform += ("arm" if isArm else "")
download_platform += ("64" if is64 else "32")
else:
download_platform = download_platform.replace("_", "-")
download_build = build
if ("" == download_build):
download_build = "latest"
download_branch = branch
if ("" == download_branch):
download_branch = "develop"
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, 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
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

@ -41,7 +41,6 @@ def make():
base.create_dir(out_dir)
# 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")
@ -56,14 +55,12 @@ def make():
base.copy_dir(base_dir + "/../sdkjs/deploy/sdkjs", out_dir + "/desktop/sdkjs")
correct_sdkjs_licence(out_dir + "/desktop/sdkjs")
base.copy_dir(base_dir + "/../web-apps/deploy/web-apps", out_dir + "/desktop/web-apps")
deldirs = ['ie', 'mobile', 'embed']
[base.delete_dir(root + "/" + d) for root, dirs, f in os.walk(out_dir + "/desktop/web-apps/apps") for d in dirs if d in deldirs]
# for bug 62528. remove empty folders
walklist = list(os.walk(out_dir + "/desktop/sdkjs"))
[os.remove(p) for p, _, _ in walklist[::-1] if len(os.listdir(p)) == 0]
base.delete_dir(out_dir + "/desktop/web-apps/apps/documenteditor/embed")
base.delete_dir(out_dir + "/desktop/web-apps/apps/documenteditor/mobile")
base.delete_dir(out_dir + "/desktop/web-apps/apps/presentationeditor/embed")
base.delete_dir(out_dir + "/desktop/web-apps/apps/presentationeditor/mobile")
base.delete_dir(out_dir + "/desktop/web-apps/apps/spreadsheeteditor/embed")
base.delete_dir(out_dir + "/desktop/web-apps/apps/spreadsheeteditor/mobile")
base.copy_file(base_dir + "/../web-apps/apps/api/documents/index.html.desktop", out_dir + "/desktop/web-apps/apps/api/documents/index.html")
build_interface(base_dir + "/../desktop-apps/common/loginpage/build")
@ -171,7 +168,6 @@ def build_js_develop(root_dir):
_run_npm(root_dir + external_folder + "/web-apps/build")
_run_npm_ci(root_dir + external_folder + "/web-apps/build/sprites")
_run_grunt(root_dir + external_folder + "/web-apps/build/sprites", [])
base.cmd_in_dir(root_dir + external_folder + "/web-apps/translation", "python", ["merge_and_check.py"])
old_cur = os.getcwd()
old_product_version = base.get_env("PRODUCT_VERSION")

View File

@ -35,41 +35,15 @@ 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):
new_replace_path = base.correctPathForBuilder(directory_builder_branding + "/docbuilder.com/src/docbuilder.h")
if ("2019" == config.option("vs-version")):
base.make_sln_project("../core/DesktopEditor/doctrenderer/docbuilder.com/src", "docbuilder.com_2019.sln")
if (True):
new_path_net = base.correctPathForBuilder(directory_builder_branding + "/docbuilder.net/src/docbuilder.net.cpp")
base.make_sln_project("../core/DesktopEditor/doctrenderer/docbuilder.net/src", "docbuilder.net.sln")
base.restorePathForBuilder(new_path_net)
else:
base.make_sln_project("../core/DesktopEditor/doctrenderer/docbuilder.com/src", "docbuilder.com.sln")
base.restorePathForBuilder(new_replace_path)
# build Java docbuilder wrapper
if config.check_option("module", "builder") and "onlyoffice" == config.branding():
for platform in platforms:
if not platform in config.platforms:
continue
# build JNI library
qmake.make(platform, base.get_script_dir() + "/../../core/DesktopEditor/doctrenderer/docbuilder.java/src/jni/docbuilder_jni.pro", "", True)
# build Java code to JAR
base.cmd_in_dir(base.get_script_dir() + "/../../core/DesktopEditor/doctrenderer/docbuilder.java", "python", ["make.py"])
new_replace_path = base.correctPathForBuilder(os.getcwd() + "/../core/DesktopEditor/doctrenderer/docbuilder.com/src/docbuilder.h")
if ("2019" == config.option("vs-version")):
base.make_sln_project("../core/DesktopEditor/doctrenderer/docbuilder.com/src", "docbuilder.com_2019.sln")
if (True):
new_path_net = base.correctPathForBuilder(os.getcwd() + "/../core/DesktopEditor/doctrenderer/docbuilder.net/src/docbuilder.net.cpp")
base.make_sln_project("../core/DesktopEditor/doctrenderer/docbuilder.net/src", "docbuilder.net.sln")
base.restorePathForBuilder(new_path_net)
else:
base.make_sln_project("../core/DesktopEditor/doctrenderer/docbuilder.com/src", "docbuilder.com.sln")
base.restorePathForBuilder(new_replace_path)
return

View File

@ -16,7 +16,6 @@ import curl
import websocket_all
import v8
import html2
import iwork
import hunspell
import glew
import harfbuzz
@ -43,7 +42,6 @@ def make():
openssl.make()
v8.make()
html2.make()
iwork.make(False)
hunspell.make(False)
harfbuzz.make()
glew.make()

View File

@ -89,15 +89,10 @@ 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"):
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("./bootstrap.sh", ["--with-libraries=filesystem,system,date_time,regex"])
base.cmd("./b2", ["headers"])
base.cmd("./b2", ["--clean"])
base.cmd("./b2", ["--prefix=./../build/linux_64", "link=static", "cxxflags=-fPIC"] + addon_compile + ["install"])
base.cmd("./b2", ["--prefix=./../build/linux_64", "link=static", "cxxflags=-fPIC", "install"])
# TODO: support x86
if config.check_option("platform", "linux_arm64") and not base.is_dir("../build/linux_arm64"):

View File

@ -7,27 +7,7 @@ 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

@ -3,11 +3,6 @@ sys.path.append('../../../scripts')
import base
import os
def clean():
if base.is_dir("hunspell"):
base.delete_dir_with_access_error("hunspell")
return
def make(build_js = True):
old_cur_dir = os.getcwd()
@ -16,8 +11,6 @@ def make(build_js = True):
core_common_dir = base.get_script_dir() + "/../../core/Common"
os.chdir(core_common_dir + "/3dParty/hunspell")
base.common_check_version("hunspell", "1", clean)
base.cmd("python", ["./before.py"])
if (build_js):

View File

@ -10,8 +10,6 @@ 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")
@ -91,13 +89,8 @@ def make():
if not base.is_dir(base_dir + "/linux_64"):
base.create_dir(base_dir + "/icu/cross_build")
os.chdir("icu/cross_build")
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("./../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")
base.cmd("make", ["-j4"])
base.cmd("make", ["install"], True)
base.create_dir(base_dir + "/linux_64")

View File

@ -1,38 +0,0 @@
#!/usr/bin/env python
import sys
sys.path.append('../..')
import config
import base
import os
import subprocess
def clear_module():
directories = ["glm", "libetonyek", "libodfgen", "librevenge", "mdds"]
for dir in directories:
if base.is_dir(dir):
base.delete_dir_with_access_error(dir)
def make(use_gperf = True):
old_cur_dir = os.getcwd()
print("[fetch & build]: iwork")
base_dir = base.get_script_dir() + "/../../core/Common/3dParty/apple"
os.chdir(base_dir)
base.check_module_version("3", clear_module)
os.chdir(old_cur_dir)
cmd_args = ["fetch.py"]
if use_gperf:
cmd_args.append("--gperf")
base.cmd_in_dir(base_dir, "python", cmd_args)
return
if __name__ == '__main__':
# manual compile
make(False)

View File

@ -81,16 +81,8 @@ 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"])
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.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

@ -45,7 +45,7 @@ def is_use_clang():
gcc_version = base.get_gcc_version()
is_clang = "false"
if (gcc_version >= 6000 or "1" == config.option("use-clang")):
if (gcc_version >= 6000):
is_clang = "true"
print("gcc version: " + str(gcc_version) + ", use clang:" + is_clang)
@ -119,7 +119,7 @@ def make():
# windows hack (delete later) ----------------------
if ("windows" == base.host_platform()):
base.delete_dir_with_access_error("v8/buildtools/win")
base.cmd("git", ["config", "--system", "core.longpaths", "true"], True)
base.cmd("git", ["config", "--system", "core.longpaths", "true"])
base.cmd("gclient", ["sync", "--force"], True)
else:
base.cmd("gclient", ["sync"], True)
@ -246,7 +246,7 @@ def make_xp():
base.cmd("./depot_tools/fetch", ["v8"], True)
base.cmd("./depot_tools/gclient", ["sync", "-r", "4.10.253"], True)
base.delete_dir_with_access_error("v8/buildtools/win")
base.cmd("git", ["config", "--system", "core.longpaths", "true"], True)
base.cmd("git", ["config", "--system", "core.longpaths", "true"])
base.cmd("gclient", ["sync", "--force"], True)
# save common py script

View File

@ -21,12 +21,6 @@ def change_bootstrap():
content += "@Subdir git\n"
content += "infra/3pp/tools/git/${platform} version:2@2.41.0.chromium.11\n"
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
@ -52,20 +46,12 @@ 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")
if "1" == config.option("use-clang"):
args_copy.append("use_sysroot=true")
linux_clang = True
else:
args_copy.append("use_sysroot=false")
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):
@ -119,9 +105,6 @@ def make():
if not base.is_dir(base_dir):
base.create_dir(base_dir)
if ("mac" == base.host_platform()):
base.cmd("git", ["config", "--global", "http.postBuffer", "157286400"], True)
os.chdir(base_dir)
if not base.is_dir("depot_tools"):
base.cmd("git", ["clone", "https://chromium.googlesource.com/chromium/tools/depot_tools.git"])
@ -138,7 +121,7 @@ def make():
base.copy_dir("./v8/third_party", "./v8/third_party_new")
if ("windows" == base.host_platform()):
os.chdir("v8")
base.cmd("git", ["config", "--system", "core.longpaths", "true"], True)
base.cmd("git", ["config", "--system", "core.longpaths", "true"])
os.chdir("../")
v8_branch_version = "remotes/branch-heads/8.9"
if ("mac" == base.host_platform()):
@ -154,11 +137,6 @@ def make():
else:
base.replaceInFile("depot_tools/gclient_paths.py", "@functools.lru_cache", "")
if ("mac" == base.host_platform()):
if not base.is_file("v8/build/config/compiler/BUILD.gn.bak"):
base.copy_file("v8/build/config/compiler/BUILD.gn", "v8/build/config/compiler/BUILD.gn.bak")
base.replaceInFile("v8/build/config/compiler/BUILD.gn", "\"-Wloop-analysis\",", "\"-Wloop-analysis\", \"-D_Float16=short\",")
if not base.is_file("v8/third_party/jinja2/tests.py.bak"):
base.copy_file("v8/third_party/jinja2/tests.py", "v8/third_party/jinja2/tests.py.bak")
base.replaceInFile("v8/third_party/jinja2/tests.py", "from collections import Mapping", "try:\n from collections.abc import Mapping\nexcept ImportError:\n from collections import Mapping")
@ -169,6 +147,7 @@ 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

@ -40,10 +40,9 @@ def make():
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, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "HtmlRenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "Fb2File")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "EpubFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "IWorkFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "HWPFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "DocxRenderer")
base.copy_file(git_dir + "/sdkjs/pdf/src/engine/cmap.bin", root_dir + "/cmap.bin")
@ -77,12 +76,9 @@ def make():
if (0 == platform.find("win")):
base.copy_file(core_build_dir + "/lib/" + platform_postfix + "/doctrenderer.lib", root_dir + "/doctrenderer.lib")
base.copy_v8_files(core_dir, root_dir, platform, isWindowsXP)
# python wrapper
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "docbuilder.c")
base.copy_file(core_dir + "/DesktopEditor/doctrenderer/docbuilder.python/src/docbuilder.py", root_dir + "/docbuilder.py")
# java wrapper
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "docbuilder.jni")
base.copy_file(core_dir + "/DesktopEditor/doctrenderer/docbuilder.java/build/libs/docbuilder.jar", root_dir + "/docbuilder.jar")
# app
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, root_dir, "docbuilder")
@ -124,25 +120,6 @@ def make():
if (0 == platform.find("mac")):
base.mac_correct_rpath_x2t(root_dir)
base.mac_correct_rpath_docbuilder(root_dir)
base.create_x2t_js_cache(root_dir, "builder", platform)
# delete unnecessary builder files
def delete_files(files):
for file in files:
base.delete_file(file)
delete_files(base.find_files(root_dir, "*.wasm"))
delete_files(base.find_files(root_dir, "*_ie.js"))
base.delete_file(root_dir + "/sdkjs/pdf/src/engine/cmap.bin")
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

@ -30,6 +30,7 @@ def make():
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "kernel_network")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "graphics")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "doctrenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "HtmlRenderer")
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, "PdfFile")
@ -37,10 +38,7 @@ def make():
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "UnicodeConverter")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "Fb2File")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "EpubFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "IWorkFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "HWPFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "DocxRenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, archive_dir, "hunspell")
base.copy_file(git_dir + "/sdkjs/pdf/src/engine/cmap.bin", archive_dir + "/cmap.bin")
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, archive_dir, "x2t")
@ -63,14 +61,13 @@ def make():
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, archive_dir, "ooxml_crypt")
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, archive_dir, "vboxtester")
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, archive_dir, "metafiletester")
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, archive_dir, "dictionariestester")
# js cache
base.generate_doctrenderer_config(archive_dir + "/DoctRenderer.config", "./", "builder", "", "./dictionaries")
base.create_x2t_js_cache(archive_dir, "core", platform)
base.delete_file(archive_dir + "/DoctRenderer.config")
# dictionaries
base.copy_dictionaries(git_dir + "/dictionaries", archive_dir + "/dictionaries", True, False)
if base.is_file(archive_dir + ".7z"):
base.delete_file(archive_dir + ".7z")
base.archive_folder(archive_dir + "/*", archive_dir + ".7z")
return

View File

@ -66,10 +66,9 @@ def make():
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", "HtmlFile2")
base.copy_lib(build_libraries_path, root_dir + "/converter", "HtmlRenderer")
base.copy_lib(build_libraries_path, root_dir + "/converter", "Fb2File")
base.copy_lib(build_libraries_path, root_dir + "/converter", "EpubFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "IWorkFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "HWPFile")
base.copy_lib(build_libraries_path, root_dir + "/converter", "DocxRenderer")
if ("ios" == platform):
@ -103,7 +102,6 @@ def make():
base.generate_doctrenderer_config(root_dir + "/converter/DoctRenderer.config", "../editors/", "desktop", "", "../dictionaries")
base.copy_dir(git_dir + "/document-templates/new", root_dir + "/converter/empty")
base.copy_dir(git_dir + "/desktop-apps/common/templates", root_dir + "/converter/templates")
# dictionaries
base.copy_dictionaries(git_dir + "/dictionaries", root_dir + "/dictionaries")
@ -184,8 +182,6 @@ 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")
elif (0 == platform.find("linux")):
@ -217,6 +213,8 @@ def make():
base.create_dir(root_dir + "/editors")
base.copy_dir(base_dir + "/js/" + branding + "/desktop/sdkjs", root_dir + "/editors/sdkjs")
if len(os.listdir(root_dir + "/editors/sdkjs")) == 0:
base.delete_dir(root_dir + "/editors/sdkjs") # delete empty folder. for bug 62528
base.copy_dir(base_dir + "/js/" + branding + "/desktop/web-apps", root_dir + "/editors/web-apps")
for file in glob.glob(root_dir + "/editors/web-apps/apps/*/*/*.js.map"):
base.delete_file(file)
@ -262,8 +260,6 @@ def make():
if isUseJSC:
base.delete_file(root_dir + "/converter/icudtl.dat")
base.create_x2t_js_cache(root_dir + "/converter", "desktop", platform)
if (0 == platform.find("win")):
base.delete_file(root_dir + "/cef_sandbox.lib")
base.delete_file(root_dir + "/libcef.lib")

View File

@ -58,11 +58,10 @@ def make():
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, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "HtmlRenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "doctrenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "Fb2File")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "EpubFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "IWorkFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "HWPFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, root_dir, "DocxRenderer")
base.copy_file(git_dir + "/sdkjs/pdf/src/engine/cmap.bin", root_dir + "/cmap.bin")

View File

@ -78,11 +78,10 @@ def make():
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, "HtmlFile2")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "HtmlRenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "doctrenderer")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "Fb2File")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "EpubFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "IWorkFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "HWPFile")
base.copy_lib(core_build_dir + "/lib/" + platform_postfix, converter_dir, "DocxRenderer")
base.copy_file(git_dir + "/sdkjs/pdf/src/engine/cmap.bin", converter_dir + "/cmap.bin")
base.copy_exe(core_build_dir + "/bin/" + platform_postfix, converter_dir, "x2t")
@ -119,8 +118,6 @@ 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", 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"])

View File

@ -5,6 +5,9 @@ import base
import os
import json
def get_core_url(arch, branch):
return "http://repo-doc-onlyoffice-com.s3.amazonaws.com/" + base.host_platform() + "/core/" + branch + "/latest/" + arch + "/core.7z"
def make():
git_dir = base.get_script_dir() + "/../.."
old_cur = os.getcwd()
@ -15,10 +18,16 @@ def make():
os.chdir(work_dir)
url = base.get_autobuild_version("core", "", config.option("branch"))
arch = "x64"
arch2 = "_64"
if ("windows" == base.host_platform()) and not base.host_platform_is64():
arch = "x86"
arch2 = "_32"
url = get_core_url(arch, config.option("branch"))
data_url = base.get_file_last_modified_url(url)
if (data_url == "" and config.option("branch") != "develop"):
url = base.get_autobuild_version("core", "", "develop")
url = get_core_url(arch, "develop")
data_url = base.get_file_last_modified_url(url)
old_data_url = base.readFile("./core.7z.data")
@ -40,6 +49,12 @@ def make():
base.extract("./core.7z", "./")
base.writeFile("./core.7z.data", data_url)
platform = ""
if ("windows" == base.host_platform()):
platform = "win" + arch2
else:
platform = base.host_platform() + arch2
base.copy_files("./core/*", "./")
else:
print("-----------------------------------------------------------")

View File

@ -539,9 +539,7 @@ def check_mysqlServer():
print(mysql_full_name + 'configuration is valid')
dependence.sqlPath = info['Location']
return dependence
print(mysql_full_name + 'configuration is not valid:' + connectionResult)
# if path exists, then further removal and installation fails(according to startup statistics). it is better to fix issue manually.
return dependence
print(mysql_full_name + 'configuration is not valid')
print('Valid MySQL Server not found')
dependence.append_uninstall('MySQL Server')

View File

@ -6,7 +6,6 @@ import base
import shutil
import optparse
import dependence
import config
arguments = sys.argv[1:]
@ -18,10 +17,6 @@ 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

@ -21,15 +21,14 @@
"core/DesktopEditor/raster/JBig2",
"core/DesktopEditor/raster/Jp2",
"core/DesktopEditor/xml/libxml2",
"core/DesktopEditor/xmlsec",
"core/DesktopEditor/xmlsec",
"core/DjVuFile/libdjvu",
"core/DjVuFile/wasm",
"core/EpubFile",
"core/OOXML/PPTXFormat/Limit/pri",
"core/Fb2File",
"core/HtmlFile2",
"core/Apple",
"core/HwpFile",
"core/HtmlFile2",
"core/OdfFile/Common/utf8cpp",
"core/OfficeUtils/js/emsdk",
"core/OfficeUtils/src/zlib-1.2.11",

View File

@ -1,32 +0,0 @@
#!/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

@ -43,7 +43,7 @@ if utils.is_linux():
},
{
"make": "rpm",
"src": "rpm/build/RPMS/*/*.rpm",
"src": "rpm/builddir/RPMS/*/*.rpm",
"dst": "builder/linux/rhel/"
}
]

View File

@ -7,17 +7,14 @@ import package_branding as branding
def make():
utils.log_h1("BUILDER")
if not (utils.is_windows() or utils.is_macos() or utils.is_linux()):
utils.log("Unsupported host OS")
return
if common.deploy:
make_archive()
if utils.is_windows():
make_windows()
elif utils.is_macos():
make_macos()
elif utils.is_linux():
make_linux()
else:
utils.log("Unsupported host OS")
return
def s3_upload(files, dst):
@ -27,133 +24,111 @@ def s3_upload(files, dst):
key = dst + utils.get_basename(f) if dst.endswith("/") else dst
upload = utils.s3_upload(f, "s3://" + branding.s3_bucket + "/" + key)
if upload:
utils.add_deploy_data(key)
utils.log("URL: " + branding.s3_base_url + "/" + key)
ret &= upload
return ret
def make_archive():
utils.set_cwd(utils.get_path(
"build_tools/out/" + common.prefix + "/" + branding.company_name.lower()))
utils.log_h2("builder archive build")
utils.delete_file("builder.7z")
args = ["7z", "a", "-y", "builder.7z", "./documentbuilder/*"]
if utils.is_windows():
ret = utils.cmd(*args, verbose=True)
else:
ret = utils.sh(" ".join(args), verbose=True)
utils.set_summary("builder archive build", ret)
utils.log_h2("builder archive deploy")
dest = "builder-" + common.prefix.replace("_","-") + ".7z"
dest_latest = "archive/%s/latest/%s" % (common.branch, dest)
dest_version = "archive/%s/%s/%s" % (common.branch, common.build, dest)
ret = utils.s3_upload(
"builder.7z", "s3://" + branding.s3_bucket + "/" + dest_version)
utils.set_summary("builder archive deploy", ret)
if ret:
utils.log("URL: " + branding.s3_base_url + "/" + dest_version)
utils.s3_copy(
"s3://" + branding.s3_bucket + "/" + dest_version,
"s3://" + branding.s3_bucket + "/" + dest_latest)
utils.log("URL: " + branding.s3_base_url + "/" + dest_latest)
utils.set_cwd(common.workspace_dir)
return
def make_windows():
global package_version, arch
global inno_file, zip_file, suffix, key_prefix
utils.set_cwd("document-builder-package")
prefix = common.platformPrefixes[common.platform]
company = branding.company_name
product = branding.builder_product_name.replace(" ","")
source_dir = "..\\build_tools\\out\\%s\\%s\\%s" % (prefix, company, product)
package_name = company + "-" + product
package_version = common.version + "." + common.build
arch = {
suffix = {
"windows_x64": "x64",
"windows_x86": "x86"
}[common.platform]
zip_file = "%s-%s-%s-%s.zip" % (company, product, package_version, suffix)
inno_file = "%s-%s-%s-%s.exe" % (company, product, package_version, suffix)
if common.clean:
utils.log_h2("builder clean")
utils.delete_dir("build")
utils.delete_files("exe\\*.exe")
utils.delete_files("zip\\*.msi")
if make_prepare():
make_zip()
make_inno()
else:
utils.set_summary("builder zip build", False)
utils.set_summary("builder inno build", False)
utils.log_h2("copy arifacts")
utils.create_dir("build\\app")
utils.copy_dir_content(source_dir, "build\\app\\")
make_zip()
make_inno()
utils.set_cwd(common.workspace_dir)
return
def make_prepare():
args = [
"-Version", package_version,
"-Arch", arch
]
if common.sign:
args += ["-Sign"]
utils.log_h2("builder prepare")
ret = utils.ps1("make.ps1", args, verbose=True)
utils.set_summary("builder prepare", ret)
return ret
def make_zip():
args = [
"-Version", package_version,
"-Arch", arch
]
# if common.sign:
# args += ["-Sign"]
utils.log_h2("builder zip build")
ret = utils.ps1("make_zip.ps1", args, verbose=True)
utils.log_h3(zip_file)
ret = utils.cmd("7z", "a", "-y", zip_file, ".\\app\\*",
chdir="build", creates="build\\" + zip_file, verbose=True)
utils.set_summary("builder zip build", ret)
if common.deploy and ret:
utils.log_h2("builder zip deploy")
ret = s3_upload(utils.glob_path("zip/*.zip"), "builder/win/generic/")
ret = s3_upload(["build\\" + zip_file], "builder/win/generic/")
utils.set_summary("builder zip deploy", ret)
return
def make_inno():
utils.log_h2("builder inno build")
utils.log_h3(inno_file)
args = [
"-Version", package_version,
"-Arch", arch
"-Arch", suffix,
"-Version", common.version,
"-Build", common.build
]
if not branding.onlyoffice:
args += ["-Branding", common.branding]
args += [
"-Branding", "%s\\%s\\document-builder-package\\exe" % (common.workspace_dir, common.branding)
]
if common.sign:
args += ["-Sign"]
utils.log_h2("builder inno build")
ret = utils.ps1("make_inno.ps1", args, verbose=True)
args += [
"-Sign",
"-CertName", branding.cert_name
]
ret = utils.ps1(
"make_inno.ps1", args, creates="build\\" + inno_file, 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/")
ret = s3_upload(["build\\" + inno_file], "builder/win/inno/")
utils.set_summary("builder inno deploy", ret)
return
def make_macos():
utils.set_cwd("document-builder-package")
company = branding.company_name.lower()
product = branding.builder_product_name.replace(" ","").lower()
source_dir = "build_tools/out/%s/%s/%s" % (common.prefix, company, product)
arch_list = {
"darwin_x86_64": "x86_64",
"darwin_arm64": "arm64"
}
suffix = arch_list[common.platform]
builder_tar = "../%s-%s-%s-%s-%s.tar.xz" % \
(company, product, common.version, common.build, suffix)
utils.log_h2("builder tar build")
make_args = ["tar"]
if common.platform == "darwin_arm64":
make_args += ["-e", "UNAME_M=arm64"]
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 tar build", ret)
utils.set_cwd(source_dir)
if common.deploy:
utils.log_h2("builder tar deploy")
ret = s3_upload(utils.glob_path("tar/*.tar.xz"), "builder/mac/generic/")
utils.set_summary("builder tar deploy", ret)
if common.clean:
utils.log_h2("builder clean")
utils.delete_files("../*.tar*")
utils.log_h2("builder build")
ret = utils.sh("tar --xz -cvf %s *" % builder_tar, creates=builder_tar, verbose=True)
utils.set_summary("builder build", ret)
if common.deploy and ret:
utils.log_h2("builder deploy")
ret = s3_upload([builder_tar], "builder/mac/generic/")
utils.set_summary("builder deploy", ret)
utils.set_cwd(common.workspace_dir)
return

View File

@ -10,37 +10,47 @@ def make():
utils.log("Unsupported host OS")
return
if common.deploy:
make_archive()
make_core()
return
def make_archive():
utils.set_cwd(utils.get_path(
"build_tools/out/" + common.prefix + "/" + branding.company_name.lower()))
def make_core():
prefix = common.platformPrefixes[common.platform]
company = branding.company_name.lower()
repos = {
"windows_x64": { "repo": "windows", "arch": "x64", "version": common.version + "." + common.build },
"windows_x86": { "repo": "windows", "arch": "x86", "version": common.version + "." + common.build },
"darwin_x86_64": { "repo": "mac", "arch": "x64", "version": common.version + "-" + common.build },
"darwin_arm64": { "repo": "mac", "arch": "arm", "version": common.version + "-" + common.build },
"linux_x86_64": { "repo": "linux", "arch": "x64", "version": common.version + "-" + common.build },
}
repo = repos[common.platform]
branch = utils.get_env("BRANCH_NAME")
core_7z = utils.get_path("build_tools/out/%s/%s/core.7z" % (prefix, company))
dest_version = "%s/core/%s/%s/%s" % (repo["repo"], branch, repo["version"], repo["arch"])
dest_latest = "%s/core/%s/%s/%s" % (repo["repo"], branch, "latest", repo["arch"])
utils.log_h2("core archive build")
utils.delete_file("core.7z")
args = ["7z", "a", "-y", "core.7z", "./core/*"]
if utils.is_windows():
ret = utils.cmd(*args, verbose=True)
else:
ret = utils.sh(" ".join(args), verbose=True)
utils.set_summary("core archive build", ret)
if branch is None:
utils.log_err("BRANCH_NAME variable is undefined")
utils.set_summary("core deploy", False)
return
if not utils.is_file(core_7z):
utils.log_err("file not exist: " + core_7z)
utils.set_summary("core deploy", False)
return
utils.log_h2("core archive deploy")
dest = "core-" + common.prefix.replace("_","-") + ".7z"
dest_latest = "archive/%s/latest/%s" % (common.branch, dest)
dest_version = "archive/%s/%s/%s" % (common.branch, common.build, dest)
utils.log_h2("core deploy")
ret = utils.s3_upload(
"core.7z", "s3://" + branding.s3_bucket + "/" + dest_version)
utils.set_summary("core archive deploy", ret)
core_7z,
"s3://" + branding.s3_bucket + "/" + dest_version + "/core.7z")
if ret:
utils.log("URL: " + branding.s3_base_url + "/" + dest_version)
utils.s3_copy(
"s3://" + branding.s3_bucket + "/" + dest_version,
"s3://" + branding.s3_bucket + "/" + dest_latest)
utils.log("URL: " + branding.s3_base_url + "/" + dest_latest)
utils.set_cwd(common.workspace_dir)
utils.log("URL: " + branding.s3_base_url + "/" + dest_version + "/core.7z")
utils.add_deploy_data(dest_version + "/core.7z")
ret = utils.s3_sync(
"s3://" + branding.s3_bucket + "/" + dest_version + "/",
"s3://" + branding.s3_bucket + "/" + dest_latest + "/",
delete=True)
utils.log("URL: " + branding.s3_base_url + "/" + dest_latest + "/core.7z")
utils.set_summary("core deploy", ret)
return
def deploy_closuremaps_sdkjs(license):
@ -64,6 +74,7 @@ def deploy_closuremaps_sdkjs(license):
ret &= upload
if upload:
utils.log("URL: " + branding.s3_base_url + "/" + key)
utils.add_deploy_data(key)
utils.set_summary("sdkjs closure maps %s deploy" % license, ret)
return
@ -89,5 +100,6 @@ def deploy_closuremaps_webapps(license):
ret &= upload
if upload:
utils.log("URL: " + branding.s3_base_url + "/" + key)
utils.add_deploy_data(key)
utils.set_summary("web-apps closure maps %s deploy" % license, ret)
return

View File

@ -26,6 +26,7 @@ def s3_upload(files, dst):
key = dst + utils.get_basename(f) if dst.endswith("/") else dst
upload = utils.s3_upload(f, "s3://" + branding.s3_bucket + "/" + key)
if upload:
utils.add_deploy_data(key)
utils.log("URL: " + branding.s3_base_url + "/" + key)
ret &= upload
return ret
@ -63,7 +64,6 @@ def make_windows():
make_zip()
make_inno()
make_advinst()
make_online()
utils.set_cwd(common.workspace_dir)
return
@ -154,6 +154,17 @@ def make_inno():
else:
ret = False
utils.set_summary("desktop inno update deploy", ret)
changes_dir = common.workspace_dir + "\\" \
+ utils.get_path(branding.desktop_changes_dir) + "\\" + common.version
if common.platform == "windows_x64" and \
common.deploy and \
utils.glob_path(changes_dir + "\\*.html"):
utils.log_h2("desktop changelog deploy")
ret = s3_upload(
utils.glob_path(changes_dir + "\\*.html"),
"desktop/win/update/%s/%s/" % (common.version, common.build))
utils.set_summary("desktop changelog deploy", ret)
return
def make_advinst():
@ -177,19 +188,6 @@ def make_advinst():
utils.set_summary("desktop advinst deploy", ret)
return
def make_online():
if not common.platform in ["windows_x86_xp"]:
return
online_file = "%s-%s-%s.exe" % ("OnlineInstaller", package_version, suffix)
ret = utils.is_file(online_file)
utils.set_summary("desktop online installer build", ret)
if common.deploy and ret:
utils.log_h2("desktop online installer deploy")
ret = s3_upload([online_file], "desktop/win/online/")
utils.set_summary("desktop online installer deploy", ret)
return
#
# macOS
#
@ -296,17 +294,11 @@ def make_sparkle_updates():
macos_zip = "build/" + zip_filename + ".zip"
utils.create_dir(updates_dir)
utils.copy_file(macos_zip, updates_dir)
utils.sh(
"ls -1t " + released_updates_dir + "/*.zip" \
+ " | head -n 3" \
+ " | while read f; do cp -fv \"$f\" " + updates_dir + "/; done",
verbose=True)
utils.copy_dir_content(released_updates_dir, updates_dir, ".zip")
for ext in [".html", ".ru.html"]:
changes_src = changes_dir + "/" + common.version + "/changes" + ext
changes_dst = updates_dir + "/" + zip_filename + ext
if not utils.copy_file(changes_src, changes_dst):
utils.write_file(changes_dst, "<!DOCTYPE html>placeholder")
for file in utils.glob_path(changes_dir + "/" + common.version + "/*.html"):
filename = utils.get_basename(file).replace("changes", zip_filename)
utils.copy_file(file, updates_dir + "/" + filename)
sparkle_base_url = "%s/%s/updates/" % (branding.sparkle_base_url, suffix)
ret = utils.sh(

View File

@ -31,6 +31,7 @@ def make_mobile():
key = "mobile/android/" + zip_file
ret = utils.s3_upload(zip_file, "s3://" + branding.s3_bucket + "/" + key)
if ret:
utils.add_deploy_data(key)
utils.log("URL: " + branding.s3_base_url + "/" + key)
utils.set_summary("mobile deploy", ret)

View File

@ -21,6 +21,7 @@ def s3_upload(files, dst):
key = dst + utils.get_basename(f) if dst.endswith("/") else dst
upload = utils.s3_upload(f, "s3://" + branding.s3_bucket + "/" + key)
if upload:
utils.add_deploy_data(key)
utils.log("URL: " + branding.s3_base_url + "/" + key)
ret &= upload
return ret
@ -36,13 +37,10 @@ def make_windows(edition):
utils.log_h2("server " + edition + " build")
ret = utils.cmd("make", "clean", verbose=True)
if edition == "prerequisites":
make_args = ["exe-pr"]
else:
make_args = ["exe", "-e", "PRODUCT_NAME=" + product_name]
args = ["-e", "PRODUCT_NAME=" + product_name]
if not branding.onlyoffice:
make_args += ["-e", "BRANDING_DIR=../" + common.branding + "/document-server-package"]
ret &= utils.cmd("make", *make_args, verbose=True)
args += ["-e", "BRANDING_DIR=../" + common.branding + "/document-server-package"]
ret &= utils.cmd("make", "packages", *args, verbose=True)
utils.set_summary("server " + edition + " build", ret)
if common.deploy and ret:

View File

@ -266,6 +266,11 @@ def set_summary(target, status):
common.summary.append({target: status})
return
def add_deploy_data(key):
with open(common.deploy_data, 'a+') as f:
f.write(key + "\n")
return
def cmd(*args, **kwargs):
if kwargs.get("verbose"):
log("- cmd:")
@ -380,13 +385,15 @@ def s3_upload(src, dst, **kwargs):
ret = sh(" ".join(args), verbose=True)
return ret
def s3_copy(src, dst, **kwargs):
def s3_sync(src, dst, **kwargs):
args = ["aws"]
if kwargs.get("endpoint_url"):
args += ["--endpoint-url", kwargs["endpoint_url"]]
args += ["s3", "cp", "--no-progress"]
args += ["s3", "sync", "--no-progress"]
if kwargs.get("acl"):
args += ["--acl", kwargs["acl"]]
if kwargs.get("delete") and kwargs["delete"]:
args += ["--delete"]
args += [src, dst]
if is_windows():
ret = cmd(*args, verbose=True)

View File

@ -29,7 +29,7 @@ def check_support_platform(platform):
return False
return True
def make(platform, project, qmake_config_addon="", is_no_errors=False):
def make(platform, project, qmake_config_addon=""):
# check platform
if not check_support_platform(platform):
print("THIS PLATFORM IS NOT SUPPORTED")
@ -93,12 +93,6 @@ def make(platform, project, qmake_config_addon="", is_no_errors=False):
qmake_app = qt_dir + "/bin/qmake"
# non windows platform
if not base.is_windows():
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")):
@ -106,7 +100,7 @@ def make(platform, project, qmake_config_addon="", is_no_errors=False):
base.cmd_and_return_cwd("make", distclean_params, True)
base.cmd(qmake_app, build_params)
base.correct_makefile_after_qmake(platform, makefile)
base.cmd_and_return_cwd("make", ["-f", makefile] + get_j_num(), is_no_errors)
base.cmd_and_return_cwd("make", ["-f", makefile] + get_j_num())
else:
config_params_array = base.qt_config_as_param(config_param)
config_params_string = ""
@ -129,7 +123,7 @@ def make(platform, project, qmake_config_addon="", is_no_errors=False):
if ("0" != config.option("multiprocess")):
qmake_bat.append("set CL=/MP")
qmake_bat.append("call nmake -f " + makefile)
base.run_as_bat(qmake_bat, is_no_errors)
base.run_as_bat(qmake_bat)
if (base.is_file(stash_file)):
base.delete_file(stash_file)

View File

@ -1,22 +1,16 @@
# 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 API using the provided Python scripts, `generate_docs_json.py` and `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.
## Requirements
## Prerequisites
```bash
Node.js v20 and above
Python v3.10 and above
```
1. **Node.js and npm**: Ensure you have Node.js and npm installed on your machine. You can download them from [Node.js official website](https://nodejs.org/).
## Installation
```bash
git clone https://github.com/ONLYOFFICE/build_tools.git
cd build_tools/scripts/sdkjs_common/jsdoc
npm install
```
2. **jsdoc**: The scripts use `jsdoc` to generate documentation. Install it using npm:
```bash
npm install
```
## Scripts Overview
@ -30,19 +24,7 @@ This script generates JSON documentation based on the `apiBuilder.js` files.
```
- **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`.
### `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`.
- `output_path` (optional): The directory where the JSON documentation will be saved. If not specified, the default path is `Onlyoffice/sdkjs/deploy/api_builder/json`.
### `generate_docs_md.py`
@ -54,7 +36,7 @@ This script generates Markdown documentation from the `apiBuilder.js` files.
```
- **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 `Onlyoffice/office-js-api`.
## Example
@ -63,11 +45,6 @@ To generate JSON documentation with the default output path:
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

View File

@ -82,11 +82,6 @@ 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,16 +0,0 @@
{
"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,16 +0,0 @@
{
"source": {
"include": ["../../../../sdkjs/common/plugins/plugin_base_api.js" ,"../../../../sdkjs/common/apiBase_plugins.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -1,85 +0,0 @@
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

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

View File

@ -1,16 +0,0 @@
{
"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,16 +0,0 @@
{
"source": {
"include": ["../../../../sdkjs/word/api_plugins.js", "../../../../sdkjs-forms/apiPlugins.js"]
},
"plugins": ["./correct_doclets.js"],
"opts": {
"destination": "./out",
"recurse": true,
"encoding": "utf8"
},
"templates": {
"json": {
"pretty": true
}
}
}

View File

@ -3,16 +3,13 @@ import subprocess
import json
import argparse
import re
import platform
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 = {
@ -23,17 +20,20 @@ editors_maps = {
}
def generate(output_dir, md=False):
missing_examples_file = f'{output_dir}/missing_examples.txt'
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Recreate missing_examples.txt file
with open(missing_examples_file, 'w', encoding='utf-8') as f:
f.write('')
# 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_set_env = "export"
if (platform.system().lower() == "windows"):
command_set_env = "set"
command = f"{command_set_env} EDITOR={editors_maps[editor_name]} && npx jsdoc -c {config} -X > {output_file}"
command = f"set EDITOR={editors_maps[editor_name]} && npx jsdoc -c {config} -X > {output_file}"
print(f"Generating {editor_name}.json: {command}")
subprocess.run(command, shell=True)
@ -55,7 +55,7 @@ def generate(output_dir, md=False):
else:
doclet['see'][0] = doclet['see'][0].replace('{Editor}', editor_name.title())
file_path = f'{root}/' + doclet['see'][0]
file_path = '../../../../' + doclet['see'][0]
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as see_file:
@ -70,20 +70,22 @@ def generate(output_dir, md=False):
comment = ''
code_content = example_content
if md == True:
doclet['example'] = remove_js_comments(comment) + "```js\n" + code_content + "\n```"
# Format content for doclet['example']
doclet['example'] = remove_js_comments(comment) + "```js\n" + code_content + "\n```"
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```'
doclet['description'] = doclet['description'] + f'\n\n## Try it\n\n ```js document-builder={{"documentType": "{editor_name.title()}"}}\n{code_content}\n```'
else:
# Record missing examples in missing_examples.txt
with open(missing_examples_file, 'a', encoding='utf-8') as missing_file:
missing_file.write(f"{file_path}\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 builder completed.")
print("Documentation generation completed.")
def remove_builder_lines(text):
lines = text.splitlines() # Split text into lines
@ -96,6 +98,25 @@ def remove_js_comments(text):
# Remove multi-line comments, leaving text after /*
text = re.sub(r'/\*\s*|\s*\*/', '', text, flags=re.DOTALL)
return text.strip()
def get_current_branch(path):
try:
# Navigate to the specified directory and get the current branch name
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
return result.stdout.strip()
else:
print(f"Error: {result.stderr}")
return None
except Exception as e:
print(f"Exception: {e}")
return None
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate documentation")
@ -104,7 +125,12 @@ 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"
default="../../../../document-builder-declarations/document-builder" # Default value
)
args = parser.parse_args()
branch_name = get_current_branch("../../../../sdkjs")
if branch_name:
args.destination = f"{args.destination}/{branch_name}"
generate(args.destination)

View File

@ -6,17 +6,12 @@ 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
editors = [
"word",
"cell",
"slide",
"forms"
]
def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
@ -27,301 +22,75 @@ def write_markdown_file(file_path, content):
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
# 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()
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}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})"
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=''):
"""
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'.
"""
def correct_description(string):
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)
# Replace opening <b> tag with **
string = re.sub(r'<b>', '**', string)
# Replace closing </b> tag with **
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
# Note
return re.sub(r'<note>(.*?)</note>', r'💡 \1', string, flags=re.DOTALL)
def correct_default_value(value, enumerations, classes):
if value is None or value == '':
if value is None:
return ''
if isinstance(value, bool):
value = "true" if value else "false"
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
return re.sub(r'[\r\n]', '', string)
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]
param_types_md = ' &#124;'.join(types)
# Set of primitive types
primitive_types = {"string", "number", "boolean", "null", "undefined", "any", "object", "false", "true", "json", "function", "{}"}
for enum in enumerations:
if enum['name'] in types:
param_types_md = param_types_md.replace(enum['name'], f"[{enum['name']}]({root}Enumeration/{enum['name']}.md)")
for cls in classes:
if cls in types:
param_types_md = param_types_md.replace(cls, f"[{cls}]({root}{cls}/{cls}.md)")
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):
def replace_with_links(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
base_type = element.split('.')[0] # Take only the first part before the dot, if any
if any(enum['name'] == base_type for enum in enumerations):
return f"<[{element}](../../Enumeration/{base_type}.md)>"
elif base_type in classes:
return f"<[{element}](../../{base_type}/{base_type}.md)>"
return f"<{element}>"
return re.sub(r'<([^<>]+)>', replace_with_links, 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 += 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']):
content += "## Methods\n\n"
for method in methods:
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.'), '../'))
# 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)
content += f"- [{method_name}](./Methods/{method_name}.md)\n"
return content
def generate_method_markdown(method, enumerations, classes, example_editor_name):
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', [])
example = method.get('example', '')
@ -329,14 +98,14 @@ def generate_method_markdown(method, enumerations, classes, example_editor_name)
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"
# Syntax section
param_list = ', '.join([param['name'] for param in params]) if params else ''
content += f"## Syntax\n\nexpression.{method_name}({param_list});\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"
@ -344,7 +113,7 @@ def generate_method_markdown(method, enumerations, classes, example_editor_name)
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.')))
param_required = "Required" if not param.get('optional') else "Optional"
param_default = correct_default_value(param.get('defaultvalue', ''), enumerations, classes)
@ -352,133 +121,91 @@ def generate_method_markdown(method, enumerations, classes, example_editor_name)
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)
return_type = ', '.join(returns[0].get('type', {}).get('names', [])) if returns[0].get('type') else 'Unknown'
# Check for enumerations and classes in return type and add links if they exist
return_type_md = generate_data_types_markdown([return_type], 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"
# Separate comment and code, and remove comment symbols
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"
return escape_text_outside_code_blocks(content)
return content
def generate_properties_markdown(properties, enumerations, classes, root='../'):
if properties is None:
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']):
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, root))
prop_types = prop['type']['names'] if prop.get('type') else []
param_types_md = generate_data_types_markdown(prop_types, enumerations, classes, root)
prop_description = remove_line_breaks(correct_description(prop_description))
param_types_md = generate_data_types_markdown(prop['type']['names'], enumerations, classes, root)
content += f"| {prop_name} | {param_types_md} | {prop_description} |\n"
content += "\n"
# Escape outside code blocks
return escape_text_outside_code_blocks(content)
return content
def generate_enumeration_markdown(enumeration, enumerations, classes, example_editor_name):
def generate_enumeration_markdown(enumeration, enumerations, classes):
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)
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
if 'TypeUnion' == enumeration['type']['parsedType']['type']:
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
elements = enumeration['type']['parsedType']['elements']
for element in elements:
element_name = element['name'] if element['type'] != 'NullLiteral' else 'null'
# Check if element is in enumerations or classes before adding link
if any(enum['name'] == element_name for enum in enumerations):
content += f"- [{element_name}](../../Enumeration/{element_name}.md)\n"
elif element_name in classes:
content += f"- [{element_name}](../../{element_name}/{element_name}.md)\n"
else:
content += f"- {element_name}\n"
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"
for t in types:
t = generate_data_types_markdown([t], enumerations, classes)
content += t + "\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"
# Separate comment and code, and remove comment symbols
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"
return escape_text_outside_code_blocks(content)
def process_doclets(data, output_dir, editor_name):
global cur_editor_name
cur_editor_name = editor_name
return content
def process_doclets(data, output_dir):
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)
classes[class_name] = []
classes_props[class_name] = doclet.get('properties', None)
elif doclet['kind'] == 'function':
class_name = doclet.get('memberof')
if class_name:
@ -490,66 +217,37 @@ def process_doclets(data, output_dir, editor_name):
# Process classes
for class_name, methods in classes.items():
if (len(methods) == 0):
continue
class_dir = os.path.join(editor_dir, class_name)
class_dir = os.path.join(output_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
)
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))
method_content = generate_method_markdown(method, enumerations, classes)
write_markdown_file(os.path.join(methods_dir, f"{method['name']}.md"), method_content)
# Process enumerations
enum_dir = os.path.join(editor_dir, 'Enumeration')
enum_dir = os.path.join(output_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))
enum_content = generate_enumeration_markdown(enum, enumerations, classes)
write_markdown_file(os.path.join(enum_dir, f"{enum['name']}.md"), enum_content)
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")
shutil.rmtree(output_dir + f'/{folder_name}', ignore_errors=True)
os.makedirs(output_dir + f'/{folder_name}')
for editor_name in editors:
input_file = os.path.join(output_dir + 'tmp_json', editor_name + ".json")
os.makedirs(output_dir + f'/{editor_name.title()}', exist_ok=True)
data = load_json(input_file)
used_enumerations.clear()
process_doclets(data, output_dir, editor_name)
process_doclets(data, output_dir + f'/{editor_name}')
shutil.rmtree(output_dir + 'tmp_json')
print('Done')
@ -564,7 +262,5 @@ if __name__ == "__main__":
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

@ -1,573 +0,0 @@
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/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})"
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=''):
"""
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 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):
# 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) 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", "{}"}
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.'), '../'))
# 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.'), '../../'))
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))
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

@ -1,111 +0,0 @@
import os
import subprocess
import json
import argparse
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"
]
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 builder 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

@ -1,618 +0,0 @@
import os
import json
import re
import shutil
import argparse
import generate_docs_plugins_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}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})"
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=''):
"""
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 correct_default_value(value, enumerations, classes):
if value is None or value == '':
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) 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", "{}"}
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.'), '../'))
# 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):
"""
Generates Markdown for a method doclet, relying only on `method['examples']`
(array of strings). Ignores any single `method['example']` field.
"""
method_name = method['name']
description = method.get('description', 'No description provided.')
description = correct_description(description, '../../')
params = method.get('params', [])
returns = method.get('returns', [])
memberof = method.get('memberof', '')
# Use the 'examples' array only
examples = method.get('examples', [])
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.'), '../../'))
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."
# 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, 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))
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):
"""
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, classes)
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"
elif raw_t in classes:
content += f"- [{raw_t}](../{raw_t}/{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, classes)
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, classes)
# 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, classes)
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 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])
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)
write_markdown_file(method_file_path, method_content)
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')
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)
if enum_content is None:
continue
write_markdown_file(enum_file_path, enum_content)
if not enum.get('examples', ''):
missing_examples.append(os.path.relpath(enum_file_path, output_dir))
def generate(output_dir):
print('Generating Markdown documentation...')
if output_dir[-1] == '/':
output_dir = output_dir[:-1]
generate_docs_plugins_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'/{folder_name}', ignore_errors=True)
os.makedirs(output_dir + f'/{folder_name}')
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="../../../../office-js-api/plugins/" # Default value
)
args = parser.parse_args()
generate(args.destination)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -1,621 +0,0 @@
import os
import json
import re
import shutil
import argparse
import generate_docs_plugins_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/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})"
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=''):
"""
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 correct_default_value(value, enumerations, classes):
if value is None or value == '':
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) 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", "{}"}
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.'), '../'))
# 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):
"""
Generates Markdown for a method doclet, relying only on `method['examples']`
(array of strings). Ignores any single `method['example']` field.
"""
method_name = method['name']
description = method.get('description', 'No description provided.')
description = correct_description(description, '../../')
params = method.get('params', [])
returns = method.get('returns', [])
memberof = method.get('memberof', '')
# Use the 'examples' array only
examples = method.get('examples', [])
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.'), '../../'))
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."
# 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, 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))
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):
"""
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, classes)
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"
elif raw_t in classes:
content += f"- [{raw_t}](../{raw_t}/{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, classes)
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, classes)
# 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, classes)
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 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])
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)
write_markdown_file(method_file_path, method_content)
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')
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)
if enum_content is None:
continue
write_markdown_file(enum_file_path, enum_content)
if not enum.get('examples', ''):
missing_examples.append(os.path.relpath(enum_file_path, output_dir))
def generate(output_dir):
print('Generating Markdown documentation...')
if output_dir[-1] == '/':
output_dir = output_dir[:-1]
generate_docs_plugins_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/plugin-and-macros/interacting-with-editors/methods/" # Default value
)
args = parser.parse_args()
generate(args.destination)
print("START_MISSING_EXAMPLES")
print(",".join(missing_examples))
print("END_MISSING_EXAMPLES")

View File

@ -1,245 +0,0 @@
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

@ -1,39 +0,0 @@
import subprocess
def fetch_branches():
#Fetch all branches without tags from the remote.
subprocess.run(['git', 'fetch', '--no-tags', 'origin', '+refs/heads/*:refs/remotes/origin/*'], check=True)
def get_branches():
#Get list of branches in the repository."""
result = subprocess.run(['git', 'branch', '-r'], capture_output=True, text=True)
return [line.strip() for line in result.stdout.splitlines()]
def parse_version(version_str):
#Parse version string and return a tuple of integers (major, minor, patch).
try:
return tuple(map(int, version_str.lstrip('v').split('.')))
except ValueError:
return (0, 0, 0) # Default for non-parsable versions
def get_max_version_branch(branches):
#Find the branch with the highest version.
max_branch = None
max_version = (0, 0, 0)
for branch in branches:
parts = branch.split('/')
if len(parts) >= 2 and (parts[1] == 'hotfix' or parts[1] == 'release'):
version = parse_version(parts[2])
if version > max_version:
max_version = version
max_branch = parts
return max_branch
if __name__ == "__main__":
fetch_branches() # Fetch branches without tags
branches = get_branches()
max_version_branch = get_max_version_branch(branches)
if max_version_branch:
print('/'.join(max_version_branch[1:])) # Print only the branch name without origin

View File

@ -14,15 +14,6 @@ def is_exist_in_array(projects, proj):
return True
return False
def get_full_projects_list(json_data, list):
result = []
for rec in list:
if rec in json_data:
result += get_full_projects_list(json_data, json_data[rec])
else:
result.append(rec)
return result
def adjust_project_params(params):
ret_params = params
@ -95,9 +86,13 @@ def get_projects(pro_json_path, platform):
# check aliases to modules
records_src = data[module]
records = get_full_projects_list(data, records_src)
records = []
#print(records)
for rec in records_src:
if rec in data:
records += data[rec]
else:
records.append(rec)
for rec in records:
params = []
@ -164,18 +159,6 @@ 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

@ -1,10 +1,6 @@
{
"root" : "../",
"spell" : [
"[win,linux,mac]core/Common/3dParty/hunspell/qt/hunspell.pro"
],
"core" : [
"core/Common/3dParty/cryptopp/project/cryptopp.pro",
@ -22,13 +18,12 @@
"core/HtmlFile2/HtmlFile2.pro",
"core/Fb2File/Fb2File.pro",
"core/EpubFile/CEpubFile.pro",
"core/HwpFile/HWPFile.pro",
"core/Apple/IWork.pro",
"core/HtmlRenderer/htmlrenderer.pro",
"core/DocxRenderer/DocxRenderer.pro",
"core/DesktopEditor/doctrenderer/doctrenderer.pro",
"core/DesktopEditor/doctrenderer/docbuilder.python/src/docbuilder_func_lib.pro",
"[!no_x2t]core/OOXML/Projects/Linux/DocxFormatLib/DocxFormatLib.pro",
"[!no_x2t]core/OOXML/Projects/Linux/PPTXFormatLib/PPTXFormatLib.pro",
@ -56,19 +51,15 @@
"[win,linux,mac,!linux_arm64]core/OfficeCryptReader/ooxml_crypt/ooxml_crypt.pro",
"spell",
"[win,linux,mac,!no_tests]core/DesktopEditor/vboxtester/vboxtester.pro",
"[win,linux,mac,!no_tests]core/Test/Applications/StandardTester/standardtester.pro",
"[win,linux,mac,!no_tests]core/Test/Applications/x2tTester/x2ttester.pro",
"[win,linux,mac,!no_tests]core/Test/Applications/MetafileTester/MetafileTester.pro",
"[win,linux,mac,!no_tests]core/Common/3dParty/hunspell/test/test.pro"
"[win,linux,mac,!no_tests]core/Test/Applications/MetafileTester/MetafileTester.pro"
],
"builder" : [
"core",
"core/DesktopEditor/doctrenderer/docbuilder.python/src/docbuilder_func_lib.pro"
"core"
],
"server" : [
@ -79,8 +70,13 @@
"[win,linux]desktop-sdk/ChromiumBasedEditors/videoplayerlib/videoplayerlib.pro"
],
"spell" : [
"[win,linux,mac]core/Common/3dParty/hunspell/qt/hunspell.pro"
],
"desktop" : [
"core",
"spell",
"multimedia",
"core/DesktopEditor/xmlsec/src/ooxmlsignature.pro",
@ -92,8 +88,7 @@
"[win,linux]desktop-apps/win-linux/ASCDocumentEditor.pro",
"[win]desktop-apps/win-linux/extras/projicons/ProjIcons.pro",
"[win,!win_xp]desktop-apps/win-linux/extras/update-daemon/UpdateDaemon.pro",
"[win_xp]desktop-apps/win-linux/extras/online-installer/OnlineInstaller.pro"
"[win,!win_xp]desktop-apps/win-linux/extras/update-daemon/UpdateDaemon.pro"
],
"mobile" : [

View File

@ -0,0 +1,4 @@
{
"browser" : "chrome",
"browserUrl" : "C:/Program Files/Google/Chrome/Application/chrome.exe"
}

View File

@ -0,0 +1,4 @@
{
"browser" : "firefox",
"browserUrl" : "C:/Program Files/Mozilla Firefox/firefox.exe"
}

View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
import sys
sys.path.append('../../scripts')
import base
import os
os.environ["PUPPETEER_SKIP_CHROMIUM_DOWNLOAD"] = "true"
base.cmd("npm", ["i", "puppeteer"])

64
tests/puppeteer/run.py Normal file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
import sys
sys.path.append('../../scripts')
import base
import os
import glob
import json
def get_tests_in_dir(directory):
files = []
for file in glob.glob(directory + "/*.js"):
if base.is_file(file):
files.append(file)
elif is_dir(file):
files += get_tests_in_dir(file)
return files
params = sys.argv[1:]
if (0 == len(params)):
print("use: run.py path_to_config [path_to_test]")
exit(0)
config_path = params[0]
test_file = "./tests"
if (1 < len(params)):
test_file = params[1]
tests_array = [test_file]
if base.is_dir(test_file):
tests_array = get_tests_in_dir(test_file)
config_content = "{}"
with open(config_path, "r") as config_path_loader:
config_content = config_path_loader.read()
print(config_content)
config = json.loads(config_content)
os.environ["PUPPETEER_SKIP_CHROMIUM_DOWNLOAD"] = "true"
if "browser" in config:
print("browser: " + config["browser"])
os.environ["PUPPETEER_PRODUCT"] = config["browser"]
if "browserUrl" in config:
print("browserUrl: " + config["browserUrl"])
os.environ["PUPPETEER_EXECUTABLE_PATH"] = config["browserUrl"]
if not base.is_dir("./work_directory"):
base.create_dir("./work_directory")
base.create_dir("./work_directory/cache")
base.create_dir("./work_directory/downloads")
for test in tests_array:
print("run test: " + test)
run_file = test + ".runned.js"
base.copy_file("./tester.js", run_file)
test_content = base.readFile(test)
test_content = test_content.replace("await Tester.", "Tester.")
test_content = test_content.replace("Tester.", "await Tester.")
base.replaceInFile(run_file, "\"%%CODE%%\"", test_content)
base.cmd("node", [run_file])
base.delete_file(run_file)

171
tests/puppeteer/tester.js Normal file
View File

@ -0,0 +1,171 @@
const puppeteer = require('puppeteer')
const pathfs = require('path')
const fs = require('fs');
function TesterImpl()
{
this.browser = null;
this.page = null;
this.width = 1500;
this.height = 800;
this.pixelRatio = 1;
this.cacheDir = pathfs.resolve("./work_directory/cache");
this.downloadsDir = pathfs.resolve("./work_directory/downloads");
this.downloadCounter = 0;
this.load = async function(url)
{
const head = { x: 100, y: 200 };
this.browser = await puppeteer.launch({
headless: false,
product: process.env["PUPPETEER_PRODUCT"],
args: [
"--disable-infobars",
`--window-size=${this.width+head.x},${this.height+head.y}`,
"--disk-cache-dir=" + this.cacheDir
],
defaultViewport : {width: this.width, height: this.height, deviceScaleFactor : this.pixelRatio }
});
this.page = await this.browser.newPage();
await this.page.setViewport({ width: this.width, height: this.height });
let waitObject = (process.env["PUPPETEER_PRODUCT"] === "firefox") ? { waitUntil: "networkidle0", timeout: 15000 } : {};
await this.page.goto(url + "&autotest=enabled", waitObject);
console.log("[tester] pageLoaded");
return this.page;
};
this.close = async function(nosleep)
{
if (true !== nosleep)
await this.waitAutosave();
await this.browser.close();
};
this.sleep = async function(ms)
{
return await new Promise(resolve => setTimeout(resolve, ms));
};
this.waitEditor = async function()
{
// TODO: wait first onEndRecalculate
await this.sleep(5000);
console.log("[tester] editorReady");
};
this.waitAutosave = async function()
{
await this.sleep(5000);
};
this.evaluateInMainFrame = async function(code)
{
return await this.page.evaluate(code);
};
this.evaluateInEditorFrame = async function(code)
{
const frame = await this.page.frames().find(frame => frame.name() === 'frameEditor');
if (!frame)
return;
return await frame.evaluate(code);
};
this.click = async function(id)
{
let res = await this.evaluateInEditorFrame("document.getElementById(\"" + id + "\").click(); \"[tester] clicked: " + id + "\"");
//console.log(res);
await this.sleep(200);
return res;
};
this.mouseClick = async function(x, y, options)
{
let res = await this.page.mouse.click(x, y, options);
await this.sleep(200);
return res;
};
this.eval = async function(code)
{
let res = await this.evaluateInEditorFrame(code);
await this.sleep(200);
return res;
};
this.keyDown = async function(key)
{
// https://pptr.dev/api/puppeteer.keyinput
let res = await this.page.keyboard.down(key);
await this.sleep(200);
return res;
};
this.keyUp = async function(key)
{
// https://pptr.dev/api/puppeteer.keyinput
let res = await this.page.keyboard.up(key);
await this.sleep(200);
return res;
};
this.keyClick = async function(key)
{
// https://pptr.dev/api/puppeteer.keyinput
let res = await this.page.keyboard.down(key);
res = await this.page.keyboard.up(key);
await this.sleep(200);
return res;
};
this.keyPress = async function(key)
{
// https://pptr.dev/api/puppeteer.keyinput
let res = await this.page.keyboard.press(key);
await this.sleep(200);
return res;
};
this.input = async function(text)
{
let res = await this.page.keyboard.type(text);
await this.sleep(200);
return res;
};
this.downloadFile = async function(format, path)
{
const tmpDir = pathfs.resolve(this.downloadsDir, "./tmp" + this.downloadCounter++);
fs.mkdirSync(tmpDir);
// emulate download
const client = await this.page.target().createCDPSession();
await client.send("Page.setDownloadBehavior", {
behavior: "allow",
downloadPath: tmpDir
});
await this.evaluateInEditorFrame("document.querySelectorAll('[data-layout-name=\"toolbar-file\"]')[0].click();");
await this.sleep(200);
await this.evaluateInEditorFrame("document.getElementsByClassName(\"svg-format-" + format + "\")[0].click();");
await this.sleep(200);
await this.evaluateInEditorFrame("document.getElementById(\"fm-btn-return\").click();");
await this.sleep(2000);
const files = fs.readdirSync(tmpDir);
fs.copyFileSync(pathfs.resolve(tmpDir, "./" + files[0]), pathfs.resolve(path));
fs.rmSync(tmpDir, { recursive: true, force: true });
};
}
const Tester = new TesterImpl;
try {
(async () => {
"%%CODE%%"
})();
} catch (err) {
console.error(err);
}

View File

@ -0,0 +1,27 @@
Tester.load("path_to_file");
Tester.waitEditor();
// down Enter
Tester.keyClick("Enter");
// type text
Tester.input("Hello World!");
Tester.keyPress("ArrowLeft");
Tester.keyDown("Shift");
for (let i = 0; i < 5; i++)
Tester.keyPress("ArrowLeft");
Tester.keyUp("Shift");
// bold
Tester.click("id-toolbar-btn-bold");
// italic
Tester.mouseClick(115, 105);
// if needed
Tester.waitAutosave();
Tester.downloadFile("docx", "./work_directory/new.docx")
Tester.downloadFile("odt", "./work_directory/new.odt")
Tester.close(true);

View File

@ -1,114 +0,0 @@
#!/usr/bin/env python
import sys
sys.path.append('../../scripts')
import base
import os
import glob
import base64
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
base.configure_common_apps()
def change_property(data_src, name, value):
data = data_src
creator_open = "<dc:" + name + ">"
creator_close = "</dc:" + name + ">"
open_tag_pos = data.find(creator_open)
if open_tag_pos == -1:
creator_close_to_find = "<dc:" + name + "/>"
else:
creator_close_to_find = "</dc:" + name + ">"
close_tag_pos = data.find(creator_close_to_find)
last_tag_pos = data.find("</cp:coreProperties>")
if open_tag_pos != -1 and close_tag_pos != - 1:
data = data[:open_tag_pos + len(creator_open)] + value + data[close_tag_pos:]
elif close_tag_pos != - 1:
data = data[:close_tag_pos] + creator_open + value + creator_close + data[close_tag_pos + len(creator_close_to_find):]
else:
data = data[:last_tag_pos] + creator_open + value + creator_close + data[last_tag_pos:]
return data
def change_author_name(file_input):
temp_dir = os.getcwd().replace("\\", "/") + "/temp"
base.create_dir(temp_dir)
app = "7za" if ("mac" == base.host_platform()) else "7z"
base.cmd_exe(app, ["x", "-y", file_input, "-o" + temp_dir, "docProps/core.xml", "-r"])
with open(temp_dir + "/docProps/core.xml", 'r', encoding='utf-8') as file:
data = file.read()
data = change_property(data, "creator", "")
data = change_property(data, "lastModifiedBy", "")
with open(temp_dir + "/docProps/core.xml", 'w', encoding='utf-8') as file:
file.write(data)
base.cmd_exe(app, ["a", "-r", file_input, temp_dir + "/docProps"])
base.delete_dir(temp_dir)
def get_files(dir):
arr_files = []
for file in glob.glob(dir + "/*"):
if base.is_file(file):
arr_files.append(file)
elif base.is_dir(file):
arr_files += get_files(file)
return arr_files
def get_local_path(base, src_dir):
test1 = base.replace("\\", "/")
test2 = src_dir.replace("\\", "/")
return test2[len(test1)+1:]
params = sys.argv[1:]
if (3 > len(params)):
print("use: convert.py path_to_x2t_directory path_to_input_directory path_to_output_directory")
exit(0)
base.configure_common_apps()
x2t_directory = params[0]
src_directory = params[1]
dst_directory = params[2]
if base.is_dir(dst_directory):
base.delete_dir(dst_directory)
base.create_dir(dst_directory)
src_files = get_files(src_directory)
for file in src_files:
directory = os.path.dirname(file)
name = os.path.basename(file)
directory_out_file = dst_directory + "/" + get_local_path(src_directory, directory)
if not base.is_dir(directory_out_file):
os.makedirs(directory_out_file, exist_ok=True)
name_without_ext = os.path.splitext(name)[0]
name_ext = os.path.splitext(name)[1][1:]
dst_ext = name_ext
if ("docx" == name_ext) or ("dotx" == name_ext):
dst_ext = "dotx"
elif ("pptx" == name_ext) or ("potx" == name_ext):
dst_ext = "potx"
elif ("xlsx" == name_ext) or ("xltx" == name_ext):
dst_ext = "xltx"
dst_name = name_without_ext
if (len(dst_name) < 4) or (dst_name[0:4] != "[32]"):
dst_name = "[32]" + base64.b32encode(name_without_ext.encode("utf-8")).decode("utf-8")
dst_file = directory_out_file + "/" + dst_name + "." + dst_ext
os.makedirs(directory_out_file, exist_ok=True)
base.cmd_in_dir(x2t_directory, "x2t", [file, dst_file])
change_author_name(dst_file)
print(name_without_ext + " => " + dst_name)

View File

@ -55,23 +55,14 @@ def install_qt():
base.cmd_in_dir("./qt-everywhere-opensource-src-5.9.9", "make", ["-j", "4"])
base.cmd_in_dir("./qt-everywhere-opensource-src-5.9.9", "make", ["install"])
return
def install_qt_prebuild():
url_amd64 = "https://s3.eu-west-1.amazonaws.com/static-doc.teamlab.eu.com/qt/5.9.9/linux_amd64/qt_binary.7z"
base.download(url_amd64, "./qt_amd64.7z")
base.extract("./qt_amd64.7z", "./qt_build")
base.create_dir("./qt_build/Qt-5.9.9")
base.cmd("mv", ["./qt_build/qt_amd64", "./qt_build/Qt-5.9.9/gcc_64"])
base.setup_local_qmake("./qt_build/Qt-5.9.9/gcc_64/bin")
return
if not base.is_file("./node_js_setup_14.x"):
print("install dependencies...")
deps.install_deps()
if not base.is_dir("./qt_build"):
if not base.is_dir("./qt_build"):
print("install qt...")
install_qt_prebuild()
install_qt()
branch = get_branch_name("../..")
@ -112,3 +103,6 @@ build_tools_params = ["--branch", branch,
base.cmd_in_dir("../..", "./configure.py", build_tools_params)
base.cmd_in_dir("../..", "./make.py")

View File

@ -39,14 +39,9 @@ def install_deps():
"libxi-dev",
"libxrender-dev",
"libxss1",
"libncurses5",
"libncurses6",
"curl",
"libxkbcommon-dev",
"libxkbcommon-x11-dev"]
"libncurses5"]
for package in packages:
base.cmd("sudo", ["apt-get", "install", "-y", package], True)
base.cmd("sudo", ["apt-get", "install", "-y"] + packages)
# nodejs
base.cmd("sudo", ["apt-get", "install", "-y", "nodejs"])
@ -84,3 +79,4 @@ def install_deps():
if __name__ == "__main__":
install_deps()

View File

@ -1 +1 @@
8.3.3
8.1.1