diff --git a/scripts/sdkjs_common/generate_builder_intarface.py b/scripts/sdkjs_common/generate_builder_intarface.py index 2fafafd0..59f259cc 100644 --- a/scripts/sdkjs_common/generate_builder_intarface.py +++ b/scripts/sdkjs_common/generate_builder_intarface.py @@ -187,8 +187,8 @@ def generate(self): self.numfile += 1 correctContent = ''.join(self.records) correctContent += "\n" - os.mkdir('deploy/api_builder/' + self.folder) - writeFile("deploy/api_builder/" + self.folder + "/api.js", correctContent) + os.mkdir('deploy/api_builder/interface/' + self.folder) + writeFile("deploy/api_builder/interface/" + self.folder + "/api.js", correctContent) return def convert_to_interface(arrFiles, sEditorType): @@ -200,8 +200,8 @@ def convert_to_interface(arrFiles, sEditorType): old_cur = os.getcwd() os.chdir("../../../sdkjs") if True == os.path.isdir('deploy/api_builder'): - shutil.rmtree('deploy/api_builder', ignore_errors=True) -os.mkdir('deploy/api_builder') + shutil.rmtree('deploy/api_builder/interface', ignore_errors=True) +os.mkdir('deploy/api_builder/interface') convert_to_interface(["word/apiBuilder.js"], "word") convert_to_interface(["word/apiBuilder.js", "slide/apiBuilder.js"], "slide") convert_to_interface(["word/apiBuilder.js", "slide/apiBuilder.js", "cell/apiBuilder.js"], "cell") diff --git a/scripts/sdkjs_common/jsdoc/README.md b/scripts/sdkjs_common/jsdoc/README.md new file mode 100644 index 00000000..5b6f5618 --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/README.md @@ -0,0 +1,57 @@ + +# Documentation Generation Guide + +This guide explains how to generate documentation for Onlyoffice API project 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. + +## Prerequisites + +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/). + +2. **jsdoc**: The scripts use `jsdoc` to generate documentation. Install it using npm: + ```bash + npm install + ``` + +## Scripts Overview + +### `generate_docs_json.py` + +This script generates JSON documentation based on the `apiBuilder.js` files. + +- **Usage**: + ```bash + python generate_docs_json.py output_path + ``` + +- **Parameters**: + - `output_path` (optional): The directory where the JSON documentation will be saved. If not specified, the default path is `Onlyoffice/sdkjs/deploy/api_builder/json`. + +### `generate_docs_md.py` + +This script generates Markdown documentation from the `apiBuilder.js` files. + +- **Usage**: + ```bash + python generate_docs_md.py output_path + ``` + +- **Parameters**: + - `output_path` (optional): The directory where the Markdown documentation will be saved. If not specified, the default path is `Onlyoffice/office-js-api`. + +## Example + +To generate JSON documentation with the default output path: +```bash +python generate_docs_json.py /path/to/save/json +``` + +To generate Markdown documentation and specify a custom output path: +```bash +python generate_docs_md.py /path/to/save/markdown +``` + +## Notes + +- Make sure to have all necessary permissions to run these scripts and write to the specified directories. +- The output directories will be created if they do not exist. + diff --git a/scripts/sdkjs_common/jsdoc/config/cell.json b/scripts/sdkjs_common/jsdoc/config/cell.json new file mode 100644 index 00000000..f30d4b66 --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/config/cell.json @@ -0,0 +1,15 @@ +{ + "source": { + "include": ["../../../../sdkjs/word/apiBuilder.js", "../../../../sdkjs/slide/apiBuilder.js", "../../../../sdkjs/cell/apiBuilder.js"] + }, + "plugins": ["./correct_doclets.js"], + "opts": { + "destination": "./out", + "recurse": true + }, + "templates": { + "json": { + "pretty": true + } + } +} diff --git a/scripts/sdkjs_common/jsdoc/config/correct_doclets.js b/scripts/sdkjs_common/jsdoc/config/correct_doclets.js new file mode 100644 index 00000000..46c79feb --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/config/correct_doclets.js @@ -0,0 +1,88 @@ +exports.handlers = { + processingComplete: function(e) { + // Инициализация массива для сохранения отфильтрованных doclets + const filteredDoclets = []; + + // Итерация по doclets и фильтрация + for (let i = 0; i < e.doclets.length; i++) { + const doclet = e.doclets[i]; + const isMethod = doclet.kind === 'function' || doclet.kind === 'method'; + const hasTypeofEditorsTag = isMethod && doclet.tags && doclet.tags.some(tag => tag.title === 'typeofeditors' && tag.value.includes(process.env.EDITOR)); + + const bAdd = doclet.kind != 'member' && (!doclet.longname || doclet.longname.search('private') == -1) && doclet.scope != "inner"; + const bSkipMethod = isMethod && !hasTypeofEditorsTag; + + if (!bSkipMethod && bAdd) { + // Оставляем только нужные поля + doclet.memberof = doclet.memberof ? doclet.memberof.replace('~', '') : doclet.memberof; + doclet.longname = doclet.longname ? doclet.longname.replace('~', '').replaceAll('"', '') : doclet.longname; + doclet.name = doclet.name ? doclet.name.replace('~', '').replaceAll('"', '') : doclet.name; + + const filteredDoclet = { + comment: doclet.comment, + description: doclet.description, + memberof: doclet.memberof ? doclet.memberof.replace('~', '') : doclet.memberof, + + 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, + + name: doclet.name, + longname: doclet.longname ? doclet.longname.replace('~', '') : doclet.longname, + kind: doclet.kind, + scope: doclet.scope, + + type: doclet.type ? { + names: doclet.type.names, + parsedType: doclet.type.parsedType + } : doclet.type, + + 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, + + meta: doclet.meta ? { + lineno: doclet.meta.lineno, + columnno: doclet.meta.columnno, + file: doclet.meta.file + } : doclet.meta, + + see: doclet.see + }; + + if (!doclet.see) { + delete doclet.see; + } + + // Добавляем отфильтрованный doclet в массив + filteredDoclets.push(filteredDoclet); + } + } + + // Заменяем doclets на отфильтрованный массив + e.doclets.splice(0, e.doclets.length, ...filteredDoclets); + } +}; diff --git a/scripts/sdkjs_common/jsdoc/config/forms.json b/scripts/sdkjs_common/jsdoc/config/forms.json new file mode 100644 index 00000000..c4e83886 --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/config/forms.json @@ -0,0 +1,16 @@ +{ + "source": { + "include": ["../../../../sdkjs-forms/apiBuilder.js"] + }, + "plugins": ["./correct_doclets.js"], + "opts": { + "destination": "./out", + "recurse": true, + "encoding": "utf8" + }, + "templates": { + "json": { + "pretty": true + } + } +} \ No newline at end of file diff --git a/scripts/sdkjs_common/jsdoc/config/slide.json b/scripts/sdkjs_common/jsdoc/config/slide.json new file mode 100644 index 00000000..2c8532b3 --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/config/slide.json @@ -0,0 +1,15 @@ +{ + "source": { + "include": ["../../../../sdkjs/word/apiBuilder.js", "../../../../sdkjs/slide/apiBuilder.js"] + }, + "plugins": ["./correct_doclets.js"], + "opts": { + "destination": "./out", + "recurse": true + }, + "templates": { + "json": { + "pretty": true + } + } +} diff --git a/scripts/sdkjs_common/jsdoc/config/word.json b/scripts/sdkjs_common/jsdoc/config/word.json new file mode 100644 index 00000000..3b90c0ad --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/config/word.json @@ -0,0 +1,16 @@ +{ + "source": { + "include": ["../../../../sdkjs/word/apiBuilder.js"] + }, + "plugins": ["./correct_doclets.js"], + "opts": { + "destination": "./out", + "recurse": true, + "encoding": "utf8" + }, + "templates": { + "json": { + "pretty": true + } + } +} \ No newline at end of file diff --git a/scripts/sdkjs_common/jsdoc/generate_docs_json.py b/scripts/sdkjs_common/jsdoc/generate_docs_json.py new file mode 100644 index 00000000..87e3836f --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/generate_docs_json.py @@ -0,0 +1,91 @@ +import os +import subprocess +import json +import argparse + +# Конфигурационные файлы +configs = [ + "./config/word.json", + "./config/cell.json", + "./config/slide.json", + "./config/forms.json" +] + +editors_maps = { + "word": "CDE", + "cell": "CSE", + "slide": "CPE", + "forms": "CFE" +} + +def generate(output_dir): + missing_examples_file = f'{output_dir}/missing_examples.txt' + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Пересоздание файла missing_examples.txt + with open(missing_examples_file, 'w', encoding='utf-8') as f: + f.write('') + + # Генерация json документации + for config in configs: + editor_name = config.split('/')[-1].replace('.json', '') + output_file = os.path.join(output_dir, editor_name + ".json") + 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) + + # дозапись примеров в json документацию + for config in configs: + editor_name = config.split('/')[-1].replace('.json', '') + output_file = os.path.join(output_dir, editor_name + ".json") + + # Чтение JSON файла + with open(output_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + # Модификация JSON данных + for doclet in data: + if 'see' in doclet: + if doclet['see'] is not None: + file_path = 'C:\\Users\\khrom\\Desktop\\Onlyoffice\\' + 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() + + # Извлечение первой строки как комментария, если она существует + 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['example'] + doclet['example'] = comment + "```js\n" + code_content + "\n```" + del doclet['see'] + else: + # Запись пропущенного примера в файл missing_examples.txt + with open(missing_examples_file, 'a', encoding='utf-8') as missing_file: + missing_file.write(f"{file_path}\n") + + # Запись измененного JSON файла обратно + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + print("Documentation generation completed.") + +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="../../../../sdkjs/deploy/api_builder/json" # Default value + ) + args = parser.parse_args() + + generate(args.destination) \ No newline at end of file diff --git a/scripts/sdkjs_common/jsdoc/generate_docs_md.py b/scripts/sdkjs_common/jsdoc/generate_docs_md.py new file mode 100644 index 00000000..a8d9dd99 --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/generate_docs_md.py @@ -0,0 +1,254 @@ +import os +import json +import re +import shutil +import argparse +import generate_docs_json + +# Конфигурационные файлы +editors = [ + "word", + "cell", + "slide", + "forms" +] + +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*//\s?', '', text, flags=re.MULTILINE) + # Удаляем многострочные комментарии, оставляя текст после /* + text = re.sub(r'/\*\s*|\s*\*/', '', text, flags=re.DOTALL) + return text.strip() + +def correct_description(string): + if string is None: + return 'No description provided.' + + # Заменяем открывающий тег на ** + string = re.sub(r'', '**', string) + # Заменяем закрывающий тег на ** + string = re.sub(r'', '**', string) + # Заметка + return re.sub(r'(.*?)', r'💡 \1', string, flags=re.DOTALL) + + +def correct_default_value(value, enumerations, classes): + if value is None: + return '' + + if value == True: + value = "true" + elif value == False: + value = "false" + else: + value = str(value) + + return generate_data_types_markdown([value], enumerations, classes) + +def remove_line_breaks(string): + return re.sub(r'[\r\n]', '', string) + +def generate_data_types_markdown(types, enumerations, classes): + param_types_md = ' |'.join(types) + + for enum in enumerations: + if enum['name'] in types: + param_types_md = param_types_md.replace(enum['name'], f"[{enum['name']}](../../Enumeration/{enum['name']}.md)") + for cls in classes: + if cls in types: + param_types_md = param_types_md.replace(cls, f"[{cls}](../../{cls}/{cls}.md)") + + def replace_with_links(match): + element = match.group(1).strip() + base_type = element.split('.')[0] # Берем только первую часть до точки, если она есть + 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): + content = f"# {class_name}\n\nRepresents the {class_name} class.\n\n" + content += "## Methods\n\n" + for method in methods: + method_name = method['name'] + content += f"- [{method_name}](./Methods/{method_name}.md)\n" + return content + +def generate_method_markdown(method, enumerations, classes): + method_name = method['name'] + description = method.get('description', 'No description provided.') + description = correct_description(description) + params = method.get('params', []) + returns = method.get('returns', []) + example = method.get('example', '') + memberof = method.get('memberof', '') + + content = f"# {method_name}\n\n{description}\n\n" + + # Syntax 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" + + 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" + + content += "\n## Returns\n\n" + if returns: + 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." + + if example: + # 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 content + +def generate_enumeration_markdown(enumeration, enumerations, classes): + enum_name = enumeration['name'] + description = enumeration.get('description', 'No description provided.') + description = correct_description(description) + example = enumeration.get('example', '') + + content = f"# {enum_name}\n\n{description}\n\n" + + if 'TypeUnion' == enumeration['type']['parsedType']['type']: + content += "## Type\n\nEnumeration\n\n" + content += "## Values\n\n" + 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 += "## Properties\n\n" + content += "| Name | Type | Description |\n" + content += "| ---- | ---- | ----------- |\n" + properties = enumeration['properties'] + 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)) + param_types_md = generate_data_types_markdown(prop['type']['names'], enumerations, classes) + content += f"| {prop_name} | {param_types_md} | {prop_description} |\n" + else: + content += "## Type\n\n" + types = enumeration['type']['names'] + for t in types: + t = generate_data_types_markdown([t], enumerations, classes) + content += t + "\n\n" + + if example: + # 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 content + +def process_doclets(data, output_dir): + classes = {} + enumerations = [] + + for doclet in data: + if doclet['kind'] == 'class': + class_name = doclet['name'] + classes[class_name] = [] + elif doclet['kind'] == 'function': + class_name = doclet.get('memberof') + if class_name: + if class_name not in classes: + classes[class_name] = [] + classes[class_name].append(doclet) + elif doclet['kind'] == 'typedef': + enumerations.append(doclet) + + # Process classes + for class_name, methods in classes.items(): + class_dir = os.path.join(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) + write_markdown_file(os.path.join(class_dir, f"{class_name}.md"), class_content) + + # Write method files + for method in methods: + 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(output_dir, 'Enumeration') + os.makedirs(enum_dir, exist_ok=True) + + for enum in enumerations: + 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') + 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) + process_doclets(data, output_dir + f'/{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/" # Default value + ) + args = parser.parse_args() + + generate(args.destination) diff --git a/scripts/sdkjs_common/jsdoc/package.json b/scripts/sdkjs_common/jsdoc/package.json new file mode 100644 index 00000000..3bda5f24 --- /dev/null +++ b/scripts/sdkjs_common/jsdoc/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "jsdoc-to-markdown": "7.1.1", + "dmd": "6.1.0", + "handlebars": "4.7.7" + } +}