#!/usr/bin/env python3 # == Rapid Develop Macros in LibreOffice == # ~ This file is part of ZAZ. # ~ ZAZ is free software: you can redistribute it and/or modify # ~ it under the terms of the GNU General Public License as published by # ~ the Free Software Foundation, either version 3 of the License, or # ~ (at your option) any later version. # ~ ZAZ is distributed in the hope that it will be useful, # ~ but WITHOUT ANY WARRANTY; without even the implied warranty of # ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # ~ GNU General Public License for more details. # ~ You should have received a copy of the GNU General Public License # ~ along with ZAZ. If not, see . import argparse import os import re import sys import zipfile from datetime import datetime from pathlib import Path from shutil import copyfile from subprocess import call from xml.etree import ElementTree as ET from xml.dom.minidom import parseString from conf import ( DATA, DIRS, DOMAIN, EXTENSION, FILES, INFO, PATHS, TYPE_EXTENSION, USE_LOCALES, log) class LiboXML(object): TYPES = { 'py': 'application/vnd.sun.star.uno-component;type=Python', 'zip': 'application/binary', 'xcu': 'application/vnd.sun.star.configuration-data', 'rdb': 'application/vnd.sun.star.uno-typelibrary;type=RDB', 'xcs': 'application/vnd.sun.star.configuration-schema', 'help': 'application/vnd.sun.star.help', 'component': 'application/vnd.sun.star.uno-components', } NAME_SPACES = { 'manifest_version': '1.2', 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', 'xmlns:loext': 'urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0', } def __init__(self): self._manifest = None self._paths = [] def _save_path(self, attr): self._paths.append(attr['{{{}}}full-path'.format(self.NAME_SPACES['manifest'])]) return def _clean(self, name, nodes): has_words = re.compile('\\w') if not re.search(has_words, str(nodes.tail)): nodes.tail = '' if not re.search(has_words, str(nodes.text)): nodes.text = '' for node in nodes: if name == 'manifest': self._save_path(node.attrib) if not re.search(has_words, str(node.tail)): node.tail = '' if not re.search(has_words, str(node.text)): node.text = '' return def new_manifest(self, data): attr = { 'manifest:version': self.NAME_SPACES['manifest_version'], 'xmlns:manifest': self.NAME_SPACES['manifest'], 'xmlns:loext': self.NAME_SPACES['xmlns:loext'], } self._manifest = ET.Element('manifest:manifest', attr) return self.add_data_manifest(data) def parse_manifest(self, data): ET.register_namespace('manifest', self.NAME_SPACES['manifest']) self._manifest = ET.fromstring(data) data = {'xmlns:loext': self.NAME_SPACES['xmlns:loext']} self._manifest.attrib.update(**data) self._clean('manifest', self._manifest) return def add_data_manifest(self, data): node_name = 'manifest:file-entry' attr = { 'manifest:full-path': '', 'manifest:media-type': '', } for path in data: if path in self._paths: continue ext = path.split('.')[-1] attr['manifest:full-path'] = path attr['manifest:media-type'] = self.TYPES.get(ext, '') ET.SubElement(self._manifest, node_name, attr) return self._get_xml(self._manifest) def _get_xml(self, doc): xml = parseString(ET.tostring(doc, encoding='utf-8')) return xml.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8') def _exists(path): return os.path.exists(path) def _join(*paths): return os.path.join(*paths) def _mkdir(path): return Path(path).mkdir(parents=True, exist_ok=True) def _save(path, data): with open(path, 'w') as f: f.write(data) return def _get_files(path, filters=''): paths = [] if filters in ('*', '*.*'): filters = '' for folder, _, files in os.walk(path): if filters: pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) paths += [_join(folder, f) for f in files if pattern.search(f)] else: paths += files return paths def _compress_oxt(): log.info('Compress OXT extension...') path = DIRS['files'] if not _exists(path): _mkdir(path) path_oxt = _join(path, FILES['oxt']) z = zipfile.ZipFile(path_oxt, 'w', compression=zipfile.ZIP_DEFLATED) root_len = len(os.path.abspath(DIRS['source'])) for root, dirs, files in os.walk(DIRS['source']): relative = os.path.abspath(root)[root_len:] for f in files: fullpath = _join(root, f) file_name = _join(relative, f) if file_name == FILES['idl']: continue z.write(fullpath, file_name, zipfile.ZIP_DEFLATED) z.close() if DATA['update']: path_xml = _join(path, FILES['update']) _save(path_xml, DATA['update']) log.info('Extension OXT created sucesfully...') return def _install_and_test(): path_oxt = (_join(DIRS['files'], FILES['oxt']),) call(PATHS['install'] + path_oxt) log.info('Install extension sucesfully...') log.info('Start LibreOffice...') call(PATHS['soffice']) return def _validate_new(): path_source = DIRS['source'] if not _exists(path_source): return True msg = f'Path: {path_source}, exists, delete first' log.error(msg) return False def _create_new_directories(): path_source = DIRS['source'] _mkdir(path_source) path = _join(path_source, DIRS['meta']) _mkdir(path) path = _join(path_source, DIRS['description']) _mkdir(path) path = _join(path_source, DIRS['images']) _mkdir(path) path = _join(path_source, DIRS['registration']) _mkdir(path) path = _join(path_source, DIRS['office']) _mkdir(path) if FILES['easymacro'] or DIRS['pythonpath']: path = _join(path_source, 'pythonpath') _mkdir(path) path = DIRS['files'] if not _exists(path): _mkdir(path) msg = 'Created directories...' log.info(msg) return def _create_new_files(): path_source = DIRS['source'] for k, v in INFO.items(): file_name = f'license_{k}.txt' path = _join(path_source, DIRS['registration'], file_name) _save(path, v['license']) if TYPE_EXTENSION > 1: path = _join(path_source, FILES['idl']) _save(path, DATA['idl']) path = _join(path_source, FILES['py']) _save(path, DATA['py']) msg = 'Created files...' log.info(msg) return def _validate_update(): if TYPE_EXTENSION == 1: return True if not _exists(PATHS['idlc']): msg = 'Binary: "idlc" not found' log.error(msg) return False if not _exists(PATHS['include']): msg = 'Directory: "include" not found' log.error(msg) return False if not _exists(PATHS['regmerge']): msg = 'Binary: "regmerge" not found' log.error(msg) return False path = _join(DIRS['source'], FILES['idl']) if not _exists(path): msg = f'File: "{FILES["idl"]}" not found' log.error(msg) return False return True def _compile_idl(): if TYPE_EXTENSION == 1: return log.info('Compilate IDL...') path_rdb = _join(DIRS['source'], FILES['rdb']) path_urd = _join(DIRS['source'], FILES['urd']) path = _join(DIRS['source'], FILES['idl']) call([PATHS['idlc'], '-I', PATHS['include'], path]) call([PATHS['regmerge'], path_rdb, '/UCR', path_urd]) os.remove(path_urd) log.info('Compilate IDL sucesfully...') return def _update_files(): path_source = DIRS['source'] for k, v in INFO.items(): file_name = FILES['ext_desc'].format(k) path = _join(path_source, DIRS['description'], file_name) _save(path, v['description']) path_logo = EXTENSION['icon'][0] if _exists(path_logo): file_name = EXTENSION['icon'][1] path = _join(path_source, DIRS['images'], file_name) copyfile(path_logo, path) files = os.listdir(DIRS['images']) for f in files: if f[-3:].lower() == 'bmp': source = _join(DIRS['images'], f) target = _join(path_source, DIRS['images'], f) copyfile(source, target) if FILES['easymacro']: source = 'easymacro.py' target = _join(path_source, 'pythonpath', source) copyfile(source, target) xml = LiboXML() path = _join(path_source, DIRS['meta'], FILES['manifest']) data = xml.new_manifest(DATA['manifest']) _save(path, data) path = _join(path_source, DIRS['office']) _mkdir(path) path = _join(path_source, DIRS['office'], FILES['shortcut']) _save(path, DATA['shortcut']) path = _join(path_source, FILES['addons']) _save(path, DATA['addons']) path = _join(path_source, FILES['description']) _save(path, DATA['description']) if TYPE_EXTENSION == 3: path = _join(path_source, FILES['addin']) _save(path, DATA['addin']) if USE_LOCALES: msg = "Don't forget generate DOMAIN.pot for locales" log.info(msg) for lang in EXTENSION['languages']: path = _join(path_source, DIRS['locales'], lang, 'LC_MESSAGES') Path(path).mkdir(parents=True, exist_ok=True) _compile_idl() return def _new(): if not _validate_new(): return _create_new_directories() _create_new_files() _update_files() msg = f"New extension: {EXTENSION['name']} make sucesfully...\n" msg += '\tNow, you can install and test: zaz.py -i' log.info(msg) return def _get_info_path(path): path, filename = os.path.split(path) name, extension = os.path.splitext(filename) return (path, filename, name, extension) def _zip_embed(source, files): PATH = 'Scripts/python/' EASYMACRO = 'easymacro.' p, f, name, e = _get_info_path(source) now = datetime.now().strftime('_%Y%m%d_%H%M%S') path_source = _join(p, name + now + e) copyfile(source, path_source) target = source with zipfile.PyZipFile(EASYMACRO + 'zip', mode='w') as zf: zf.writepy(EASYMACRO + 'py') xml = LiboXML() path_easymacro = PATH + EASYMACRO + 'zip' names = [f[1] for f in files] + [path_easymacro] nodes = [] with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as zt: with zipfile.ZipFile(path_source, compression=zipfile.ZIP_DEFLATED) as zs: for name in zs.namelist(): if FILES['manifest'] in name: path_manifest = name xml_manifest = zs.open(name).read() elif name in names: continue else: zt.writestr(name, zs.open(name).read()) data = [] for path, name in files: data.append(name) zt.write(path, name) zt.write(EASYMACRO + 'zip', path_easymacro) data.append(path_easymacro) xml.parse_manifest(xml_manifest) xml_manifest = xml.add_data_manifest(data) zt.writestr(path_manifest, xml_manifest) os.unlink(EASYMACRO + 'zip') return def _embed(args): PATH = 'Scripts/python' PYTHONPATH = 'pythonpath' doc = args.document if not doc: msg = '-d/--document Path file to embed is mandatory' log.error(msg) return if not _exists(doc): msg = 'Path file not exists' log.error(msg) return files = [] if args.files: files = args.files.split(',') source = _join(PATHS['profile'], PATH) content = os.listdir(source) if PYTHONPATH in content: content.remove(PYTHONPATH) if files: files = [(_join(source, f), _join(PATH, f)) for f in files if f in content] else: files = [(_join(source, f), _join(PATH, f)) for f in content] _zip_embed(doc, files) log.info('Embedded macros successfully...') return def _locales(args): EASYMACRO = 'easymacro.py' if args.files: files = args.files.split(',') else: files = _get_files(DIRS['source'], 'py') paths = ' '.join([f for f in files if not EASYMACRO in f]) path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) call([PATHS['gettext'], '-o', path_pot, paths]) log.info('POT generate successfully...') return def _update(): path_locales = _join(DIRS['source'], DIRS['locales']) path_pot = _join(DIRS['source'], DIRS['locales'], '{}.pot'.format(DOMAIN)) if not _exists(path_pot): log.error('Not exists file POT...') return files = _get_files(path_locales, 'po') if not files: log.error('First, generate files PO...') return for f in files: call([PATHS['msgmerge'], '-U', f, path_pot]) log.info('\tUpdate: {}'.format(f)) log.info('Locales update successfully...') return def main(args): if args.update: _update() return if args.locales: _locales(args) return if args.embed: _embed(args) return if args.new: _new() return if not _validate_update(): return _update_files() _compress_oxt() if args.install: _install_and_test() log.info('Extension make successfully...') return def _process_command_line_arguments(): parser = argparse.ArgumentParser( description='Make LibreOffice extensions') parser.add_argument('-i', '--install', dest='install', action='store_true', default=False, required=False) parser.add_argument('-n', '--new', dest='new', action='store_true', default=False, required=False) parser.add_argument('-e', '--embed', dest='embed', action='store_true', default=False, required=False) parser.add_argument('-d', '--document', dest='document', default='') parser.add_argument('-f', '--files', dest='files', default='') parser.add_argument('-l', '--locales', dest='locales', action='store_true', default=False, required=False) parser.add_argument('-u', '--update', dest='update', action='store_true', default=False, required=False) return parser.parse_args() if __name__ == '__main__': args = _process_command_line_arguments() main(args)