Start develop

This commit is contained in:
Mauricio Baeza 2021-07-12 21:30:59 -05:00
commit ab52900689
4 changed files with 8636 additions and 0 deletions

437
conf.py Normal file
View File

@ -0,0 +1,437 @@
#!/usr/bin/env python3
# ~ 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 <https://www.gnu.org/licenses/>.
import logging
# ~ Type extension:
# ~ 1 = normal extension
# ~ 2 = new component
# ~ 3 = Calc addin
TYPE_EXTENSION = 1
# ~ Your great extension name, not used spaces
NAME = 'MyFirstExtension'
# ~ https://semver.org/
VERSION = '0.1.0'
# ~ Should be unique, used URL inverse
ID = 'org.myfirstextension'
# ~ If you extension will be multilanguage set: True
# ~ This feature used gettext, set pythonpath and easymacro in True
# ~ You can used PoEdit for edit PO files and generate MO files.
# ~ https://poedit.net/
USE_LOCALES = True
DOMAIN = 'base'
PATH_LOCALES = 'locales'
PATH_PYGETTEXT = '/usr/lib/python3.9/Tools/i18n/pygettext.py'
# ~ You can use PoEdit for update locales too
PATH_MSGMERGE = 'msgmerge'
# ~ Show in extension manager
PUBLISHER = {
'en': {'text': 'El Mau', 'link': 'https://gitlab.com/mauriciobaeza'},
'es': {'text': 'El Mau', 'link': 'https://gitlab.com/mauriciobaeza'},
}
# ~ Name in this folder for copy
ICON = 'images/logo.png'
# ~ Name inside extensions
ICON_EXT = f'{NAME.lower()}.png'
# ~ Change for you favorite license
LICENSE_EN = f"""This file is part of {NAME}.
{NAME} 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.
{NAME} 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 {NAME}. If not, see <https://www.gnu.org/licenses/>.
"""
LICENSE_ES = LICENSE_EN
INFO = {
'en': {
'display_name': 'My first extension',
'description': 'My great extension',
'license': LICENSE_EN,
},
'es': {
'display_name': 'Mi primer extensión',
'description': 'Mi gran extensión',
'license': LICENSE_ES,
},
}
# ~ Menus, only for TYPE_EXTENSION = 1
# ~ Parent can be: AddonMenu or OfficeMenuBar
# ~ For icons con name: NAME_16.bmp, used only NAME
# ~ PARENT = ''
# ~ MENU_MAIN = {}
# ~ Shortcut: Key + "Modifier Keys"
# ~ Important: Not used any shortcuts used for LibreOffice
# ~ SHIFT is mapped to Shift on all platforms.
# ~ MOD1 is mapped to Ctrl on Windows/Linux, while it is mapped to Cmd on Mac.
# ~ MOD2 is mapped to Alt on all platforms.
# ~ For example: Shift+Ctrl+Alt+T -> T_SHIFT_MOD1_MOD2
PARENT = 'OfficeMenuBar'
MENU_MAIN = {
'en': 'My Extension',
'es': 'Mi Extensión',
}
MENUS = (
{
'title': {'en': 'Option 1', 'es': 'Opción 1'},
'argument': 'option1',
'context': 'calc,writer',
'icon': 'icon',
'toolbar': True,
'shortcut': 'T_SHIFT_MOD1_MOD2',
},
{
'title': {'en': 'Submenu', 'es': 'Submenu'},
'context': 'calc,writer',
'submenu': (
{
'title': {'en': 'Option 2', 'es': 'Opción 2'},
'argument': 'option2',
'context': 'calc,writer',
'icon': 'icon',
'toolbar': True,
'shortcut': '',
},
),
},
)
# ~ Functions, only for TYPE_EXTENSION = 3
FUNCTIONS = {
'test': {
'displayname': {'en': 'test', 'es': 'prueba'},
'description': {'en': 'My test', 'es': 'Mi prueba'},
'parameters': {
'value': {
'displayname': {'en': 'value', 'es': 'valor'},
'description': {'en': 'The value', 'es': 'El valor'},
},
},
},
}
# ~ FUNCTIONS = {}
EXTENSION = {
'version': VERSION,
'name': NAME,
'id': ID,
'icon': (ICON, ICON_EXT),
'languages': tuple(INFO.keys())
}
# ~ If used more libraries set python path in True and copy inside
# ~ If used easymacro pythonpath always is True, recommended
DIRS = {
'meta': 'META-INF',
'source': 'source',
'description': 'description',
'images': 'images',
'registration': 'registration',
'files': 'files',
'office': 'Office',
'locales': PATH_LOCALES,
'pythonpath': True,
}
FILES = {
'oxt': f'{NAME}_v{VERSION}.oxt',
'py': f'{NAME}.py',
'ext_desc': 'desc_{}.txt',
'manifest': 'manifest.xml',
'description': 'description.xml',
'idl': f'X{NAME}.idl',
'addons': 'Addons.xcu',
'urd': f'X{NAME}.urd',
'rdb': f'X{NAME}.rdb',
'update': f'{NAME.lower()}.update.xml',
'addin': 'CalcAddIn.xcu',
'shortcut': 'Accelerators.xcu',
'easymacro': True,
}
# ~ URLs for update for example
# ~ URL_XML_UPDATE = 'https://gitlab.com/USER/PROYECT/raw/BRANCH/FOLDERs/FILE_NAME'
URL_XML_UPDATE = ''
URL_OXT = ''
# ~ Default program for test: --calc, --writer, --draw
PROGRAM = '--calc'
# ~ Path to file for test
FILE_TEST = ''
PATHS = {
'idlc': '/usr/lib/libreoffice/sdk/bin/idlc',
'include': '/usr/share/idl/libreoffice',
'regmerge': '/usr/lib/libreoffice/program/regmerge',
'soffice': ('soffice', PROGRAM, FILE_TEST),
'install': ('unopkg', 'add', '-v', '-f', '-s'),
'profile': '/home/mau/.config/libreoffice/4/user',
'gettext': PATH_PYGETTEXT,
'msgmerge': PATH_MSGMERGE,
}
SERVICES = {
'job': "('com.sun.star.task.Job',)",
'addin': "('com.sun.star.sheet.AddIn',)",
}
FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
DATE = '%d/%m/%Y %H:%M:%S'
LEVEL_ERROR = logging.getLevelName(logging.ERROR)
LEVEL_INFO = logging.getLevelName(logging.INFO)
logging.addLevelName(logging.ERROR, f'\033[1;41m{LEVEL_ERROR}\033[1;0m')
logging.addLevelName(logging.INFO, f'\x1b[32m{LEVEL_INFO}\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=FORMAT, datefmt=DATE)
log = logging.getLogger(NAME)
def _methods():
template = """ def {0}(self, {1}):
print({1})
return 'ok'\n"""
functions = ''
for k, v in FUNCTIONS.items():
args = ','.join(v['parameters'].keys())
functions += template.format(k, args)
return functions
SRV = SERVICES['job']
XSRV = 'XJobExecutor'
SRV_IMPORT = f'from com.sun.star.task import {XSRV}'
METHODS = """ def trigger(self, args='pyUNO'):
print('Hello World', args)
return\n"""
if TYPE_EXTENSION > 1:
MENUS = ()
XSRV = f'X{NAME}'
SRV_IMPORT = f'from {ID} import {XSRV}'
if TYPE_EXTENSION == 2:
SRV = f"('{ID}',)"
METHODS = """ def test(self, args='pyUNO'):
print('Hello World', args)
return\n"""
elif TYPE_EXTENSION == 3:
SRV = SERVICES['addin']
METHODS = _methods()
DATA_PY = f"""import uno
import unohelper
{SRV_IMPORT}
ID_EXTENSION = '{ID}'
SERVICE = {SRV}
class {NAME}(unohelper.Base, {XSRV}):
def __init__(self, ctx):
self.ctx = ctx
{METHODS}
g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation({NAME}, ID_EXTENSION, SERVICE)
"""
def _functions():
a = '[in] any {}'
t = ' any {}({});'
f = ''
for k, v in FUNCTIONS.items():
args = ','.join([a.format(k) for k, v in v['parameters'].items()])
f += t.format(k, args)
return f
FILE_IDL = ''
if TYPE_EXTENSION > 1:
id_ext = ID.replace('.', '_')
interface = f'X{NAME}'
module = ''
for i, P in enumerate(ID.split('.')):
module += f'module {P} {{ '
close_module = '}; ' * (i + 1)
functions = ' void test([in] any argument);'
if TYPE_EXTENSION == 3:
functions = _functions()
FILE_IDL = f"""#ifndef __{id_ext}_idl__
#define __{id_ext}_idl__
#include <com/sun/star/uno/XInterface.idl>
{module}
interface {interface} : com::sun::star::uno::XInterface
{{
{functions}
}};
service {P} {{
interface {interface};
}};
{close_module}
#endif
"""
def _parameters(args):
NODE = """ <node oor:name="{name}" oor:op="replace">
<prop oor:name="DisplayName">
{displayname}
</prop>
<prop oor:name="Description">
{description}
</prop>
</node>"""
line = '{}<value xml:lang="{}">{}</value>'
node = ''
for k, v in args.items():
displayname = '\n'.join(
[line.format(' ' * 16, k, v) for k, v in v['displayname'].items()])
description = '\n'.join(
[line.format(' ' * 16, k, v) for k, v in v['description'].items()])
values = {
'name': k,
'displayname': displayname,
'description': description,
}
node += NODE.format(**values)
return node
NODE_FUNCTIONS = ''
if TYPE_EXTENSION == 3:
tmp = '{}<value xml:lang="{}">{}</value>'
NODE_FUNCTION = """ <node oor:name="{name}" oor:op="replace">
<prop oor:name="DisplayName">
{displayname}
</prop>
<prop oor:name="Description">
{description}
</prop>
<prop oor:name="Category">
<value>Add-In</value>
</prop>
<prop oor:name="CompatibilityName">
<value xml:lang="en">AutoAddIn.{name}</value>
</prop>
<node oor:name="Parameters">
{parameters}
</node>
</node>"""
for k, v in FUNCTIONS.items():
displayname = '\n'.join(
[tmp.format(' ' * 12, k, v) for k, v in v['displayname'].items()])
description = '\n'.join(
[tmp.format(' ' * 12, k, v) for k, v in v['description'].items()])
parameters = _parameters(v['parameters'])
values = {
'name': k,
'displayname': displayname,
'description': description,
'parameters': parameters,
}
NODE_FUNCTIONS += NODE_FUNCTION.format(**values)
FILE_ADDIN = f"""<?xml version="1.0" encoding="UTF-8"?>
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="CalcAddIns" oor:package="org.openoffice.Office">
<node oor:name="AddInInfo">
<node oor:name="{ID}" oor:op="replace">
<node oor:name="AddInFunctions">
{NODE_FUNCTIONS}
</node>
</node>
</node>
</oor:component-data>"""
DATA_MANIFEST = [FILES['py'], f"Office/{FILES['shortcut']}", 'Addons.xcu']
if TYPE_EXTENSION > 1:
DATA_MANIFEST.append(FILES['rdb'])
if TYPE_EXTENSION == 3:
DATA_MANIFEST.append('CalcAddIn.xcu')
DATA_DESCRIPTION = {
'identifier': {'value': ID},
'version': {'value': VERSION},
'display-name': {k: v['display_name'] for k, v in INFO.items()},
'icon': ICON_EXT,
'publisher': PUBLISHER,
'update': URL_XML_UPDATE,
}
DATA_ADDONS = {
'parent': PARENT,
'images': DIRS['images'],
'main': MENU_MAIN,
'menus': MENUS,
}
DATA = {
'py': DATA_PY,
'manifest': DATA_MANIFEST,
'description': DATA_DESCRIPTION,
'addons': DATA_ADDONS,
'update': URL_OXT,
'idl': FILE_IDL,
'addin': FILE_ADDIN,
}
with open('VERSION', 'w') as f:
f.write(VERSION)
# ~ LICENSE_ACCEPT_BY = 'user' # or admin
# ~ LICENSE_SUPPRESS_ON_UPDATE = True

7377
easymacro.py Normal file

File diff suppressed because it is too large Load Diff

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

822
zaz.py Normal file
View File

@ -0,0 +1,822 @@
#!/usr/bin/env python3
# == Rapid Develop Macros in LibreOffice ==
# ~ This file is part of ZAZ.
# ~ https://git.elmau.net/elmau/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 <https://www.gnu.org/licenses/>.
import argparse
import os
import py_compile
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)
EASYMACRO = 'easymacro.py'
class LiboXML(object):
CONTEXT = {
'calc': 'com.sun.star.sheet.SpreadsheetDocument',
'writer': 'com.sun.star.text.TextDocument',
'impress': 'com.sun.star.presentation.PresentationDocument',
'draw': 'com.sun.star.drawing.DrawingDocument',
'base': 'com.sun.star.sdb.OfficeDatabaseDocument',
'math': 'com.sun.star.formula.FormulaProperties',
'basic': 'com.sun.star.script.BasicIDE',
}
TYPES = {
'py': 'application/vnd.sun.star.uno-component;type=Python',
'pyc': 'application/binary',
'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',
}
NS_MANIFEST = {
'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',
}
NS_DESCRIPTION = {
'xmlns': 'http://openoffice.org/extensions/description/2006',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'xmlns:d': 'http://openoffice.org/extensions/description/2006',
}
NS_ADDONS = {
'xmlns:xs': 'http://www.w3.org/2001/XMLSchema',
'xmlns:oor': 'http://openoffice.org/2001/registry',
}
NS_UPDATE = {
'xmlns': 'http://openoffice.org/extensions/update/2006',
'xmlns:d': 'http://openoffice.org/extensions/description/2006',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
}
def __init__(self):
self._manifest = None
self._paths = []
self._path_images = ''
self._toolbars = []
def _save_path(self, attr):
self._paths.append(attr['{{{}}}full-path'.format(self.NS_MANIFEST['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.NS_MANIFEST['manifest_version'],
'xmlns:manifest': self.NS_MANIFEST['manifest'],
'xmlns:loext': self.NS_MANIFEST['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.NS_MANIFEST['manifest'])
self._manifest = ET.fromstring(data)
attr = {'xmlns:loext': self.NS_MANIFEST['xmlns:loext']}
self._manifest.attrib.update(**attr)
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 new_description(self, data):
doc = ET.Element('description', self.NS_DESCRIPTION)
key = 'identifier'
ET.SubElement(doc, key, data[key])
key = 'version'
ET.SubElement(doc, key, data[key])
key = 'display-name'
node = ET.SubElement(doc, key)
for k, v in data[key].items():
sn = ET.SubElement(node, 'name', {'lang': k})
sn.text = v
node = ET.SubElement(doc, 'extension-description')
for k in data[key].keys():
attr = {
'lang': k,
'xlink:href': f'description/desc_{k}.txt',
}
ET.SubElement(node, 'src', attr)
key = 'icon'
node = ET.SubElement(doc, key)
attr = {'xlink:href': f"images/{data[key]}"}
ET.SubElement(node, 'default', attr)
key = 'publisher'
node = ET.SubElement(doc, key)
for k, v in data[key].items():
attr = {
'xlink:href': v['link'],
'lang': k,
}
sn = ET.SubElement(node, 'name', attr)
sn.text = v['text']
key = 'display-name'
node = ET.SubElement(doc, 'registration')
attr = {
'accept-by': 'user',
'suppress-on-update': 'true',
}
node = ET.SubElement(node, 'simple-license', attr)
for k in data[key].keys():
attr = {
'xlink:href': f"{DIRS['registration']}/license_{k}.txt",
'lang': k
}
ET.SubElement(node, 'license-text', attr)
if data['update']:
node = ET.SubElement(doc, 'update-information')
ET.SubElement(node, 'src', {'xlink:href': data['update']})
return self._get_xml(doc)
def _get_context(self, args):
if not args:
return ''
context = ','.join([self.CONTEXT[v] for v in args.split(',')])
return context
def _add_node_value(self, node, name, value='_self'):
attr = {'oor:name': name, 'oor:type': 'xs:string'}
sn = ET.SubElement(node, 'prop', attr)
sn = ET.SubElement(sn, 'value')
sn.text = value
return
def _add_menu(self, id_extension, node, index, menu, in_menu_bar=True):
if in_menu_bar:
attr = {
'oor:name': index,
'oor:op': 'replace',
}
subnode = ET.SubElement(node, 'node', attr)
else:
subnode = node
attr = {'oor:name': 'Title', 'oor:type': 'xs:string'}
sn1 = ET.SubElement(subnode, 'prop', attr)
for k, v in menu['title'].items():
sn2 = ET.SubElement(sn1, 'value', {'xml:lang': k})
sn2.text = v
value = self._get_context(menu['context'])
self._add_node_value(subnode, 'Context', value)
if 'submenu' in menu:
sn = ET.SubElement(subnode, 'node', {'oor:name': 'Submenu'})
for i, m in enumerate(menu['submenu']):
self._add_menu(id_extension, sn, f'{index}.s{i}', m)
if m.get('toolbar', False):
self._toolbars.append(m)
return
value = f"service:{id_extension}?{menu['argument']}"
self._add_node_value(subnode, 'URL', value)
self._add_node_value(subnode, 'Target')
value = f"%origin%/{self._path_images}/{menu['icon']}"
self._add_node_value(subnode, 'ImageIdentifier', value)
return
def new_addons(self, id_extension, data):
in_menu_bar = data['parent'] == 'OfficeMenuBar'
self._path_images = data['images']
attr = {
'oor:name': 'Addons',
'oor:package': 'org.openoffice.Office',
}
attr.update(self.NS_ADDONS)
doc = ET.Element('oor:component-data', attr)
parent = ET.SubElement(doc, 'node', {'oor:name': 'AddonUI'})
node = ET.SubElement(parent, 'node', {'oor:name': data['parent']})
op = 'fuse'
if in_menu_bar:
op = 'replace'
attr = {'oor:name': id_extension, 'oor:op': op}
node = ET.SubElement(node, 'node', attr)
if in_menu_bar:
attr = {'oor:name': 'Title', 'oor:type': 'xs:string'}
subnode = ET.SubElement(node, 'prop', attr)
for k, v in data['main'].items():
sn = ET.SubElement(subnode, 'value', {'xml:lang': k})
sn.text = v
self._add_node_value(node, 'Target')
node = ET.SubElement(node, 'node', {'oor:name': 'Submenu'})
for i, menu in enumerate(data['menus']):
self._add_menu(id_extension, node, f'm{i}', menu, in_menu_bar)
if menu.get('toolbar', False):
self._toolbars.append(menu)
if self._toolbars:
attr = {'oor:name': 'OfficeToolBar'}
toolbar = ET.SubElement(parent, 'node', attr)
attr = {'oor:name': id_extension, 'oor:op': 'replace'}
toolbar = ET.SubElement(toolbar, 'node', attr)
for t, menu in enumerate(self._toolbars):
self._add_menu(id_extension, toolbar, f't{t}', menu)
return self._get_xml(doc)
def _add_shortcut(self, node, key, id_extension, arg):
attr = {'oor:name': key, 'oor:op': 'fuse'}
subnode = ET.SubElement(node, 'node', attr)
subnode = ET.SubElement(subnode, 'prop', {'oor:name': 'Command'})
subnode = ET.SubElement(subnode, 'value', {'xml:lang': 'en-US'})
subnode.text = f"service:{id_extension}?{arg}"
return
def _get_acceleartors(self, menu):
if 'submenu' in menu:
for m in menu['submenu']:
return self._get_acceleartors(m)
if not menu.get('shortcut', ''):
return ''
return menu
def new_accelerators(self, id_extension, menus):
attr = {
'oor:name': 'Accelerators',
'oor:package': 'org.openoffice.Office',
}
attr.update(self.NS_ADDONS)
doc = ET.Element('oor:component-data', attr)
parent = ET.SubElement(doc, 'node', {'oor:name': 'PrimaryKeys'})
data = []
for m in menus:
info = self._get_acceleartors(m)
if info:
data.append(info)
node_global = None
node_modules = None
for m in data:
if m['context']:
if node_modules is None:
node_modules = ET.SubElement(
parent, 'node', {'oor:name': 'Modules'})
for app in m['context'].split(','):
node = ET.SubElement(
node_modules, 'node', {'oor:name': self.CONTEXT[app]})
self._add_shortcut(
node, m['shortcut'], id_extension, m['argument'])
else:
if node_global is None:
node_global = ET.SubElement(
parent, 'node', {'oor:name': 'Global'})
self._add_shortcut(
node_global, m['shortcut'], id_extension, m['argument'])
return self._get_xml(doc)
def new_update(self, extension, url_oxt):
doc = ET.Element('description', self.NS_UPDATE)
ET.SubElement(doc, 'identifier', {'value': extension['id']})
ET.SubElement(doc, 'version', {'value': extension['version']})
node = ET.SubElement(doc, 'update-download')
ET.SubElement(node, 'src', {'xlink:href': url_oxt})
node = ET.SubElement(doc, 'release-notes')
return self._get_xml(doc)
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_oxt = _join(DIRS['files'], 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()
log.info('Extension OXT created successfully...')
return
def _install_and_test():
path_oxt = (_join(DIRS['files'], FILES['oxt']),)
call(PATHS['install'] + path_oxt)
log.info('Install extension successfully...')
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 successfully...')
return
def _update_files():
path_files = DIRS['files']
if not _exists(path_files):
_mkdir(path_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
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, FILES['description'])
data = xml.new_description(DATA['description'])
_save(path, data)
if TYPE_EXTENSION == 1:
path = _join(path_source, FILES['addons'])
data = xml.new_addons(EXTENSION['id'], DATA['addons'])
_save(path, data)
path = _join(path_source, DIRS['office'])
_mkdir(path)
path = _join(path_source, DIRS['office'], FILES['shortcut'])
data = xml.new_accelerators(EXTENSION['id'], DATA['addons']['menus'])
_save(path, data)
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"
for lang in EXTENSION['languages']:
path = _join(path_source, DIRS['locales'], lang, 'LC_MESSAGES')
Path(path).mkdir(parents=True, exist_ok=True)
log.info(msg)
if DATA['update']:
path_xml = _join(path_files, FILES['update'])
data = xml.new_update(EXTENSION, DATA['update'])
_save(path_xml, data)
_compile_idl()
return
def _create():
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/'
FILE_PYC = 'easymacro.pyc'
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
py_compile.compile(EASYMACRO, FILE_PYC)
xml = LiboXML()
path_easymacro = PATH + FILE_PYC
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(FILE_PYC, 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(FILE_PYC)
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):
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 _new(args):
if not args.target:
msg = 'Add argument target: -t PATH_TARGET'
log.error(msg)
return
if not args.name:
msg = 'Add argument name: -n name-new-extension'
log.error(msg)
return
path = _join(args.target, args.name)
_mkdir(path)
_mkdir(_join(path, 'files'))
_mkdir(_join(path, 'images'))
path_logo = 'images/pymacros.png'
copyfile(path_logo, _join(path, 'images/logo.png'))
copyfile('zaz.py', _join(path, 'zaz.py'))
copyfile(EASYMACRO, _join(path, 'easymacro.py'))
copyfile('conf.py.example', _join(path, 'conf.py'))
msg = 'Folders and files copy successfully for new extension.'
log.info(msg)
msg = f'Change to folder: {path}'
log.info(msg)
return
def main(args):
if args.new:
_new(args)
return
if args.update:
_update()
return
if args.locales:
_locales(args)
return
if args.embed:
_embed(args)
return
if args.create:
_create()
return
if not _validate_update():
return
if not args.only_compress:
_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('-new', '--new', dest='new', action='store_true',
default=False, required=False)
parser.add_argument('-t', '--target', dest='target', default='')
parser.add_argument('-n', '--name', dest='name', default='', required=False)
parser.add_argument('-c', '--create', dest='create', action='store_true',
default=False, required=False)
parser.add_argument('-i', '--install', dest='install', 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)
parser.add_argument('-oc', '--only_compress', dest='only_compress',
action='store_true', default=False, required=False)
return parser.parse_args()
if __name__ == '__main__':
args = _process_command_line_arguments()
main(args)