First functional version

This commit is contained in:
Mauricio Baeza 2021-10-07 14:22:30 -05:00
parent ae1a5cdbe5
commit 2688ba6461
25 changed files with 17383 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
*.py[cod]
*.po~
*.log
# Virtualenv
.env/
virtual/

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.1.0

439
conf.py Normal file
View File

@ -0,0 +1,439 @@
#!/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 = 'ZAZPass'
# ~ https://semver.org/
VERSION = '0.1.0'
# ~ Should be unique, used URL inverse
ID = 'net.elmau.zaz.pass'
# ~ 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://git.cuates.net/elmau'},
'es': {'text': 'El Mau', 'link': 'https://git.cuates.net/elmau'},
}
# ~ 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': 'Zaz Passwords',
'es': 'Zaz Contraseñas',
}
MENUS = (
{
'title': {'en': 'Insert password', 'es': 'Insertar contraseña'},
'argument': 'insert',
'context': 'calc,writer',
'icon': 'icon',
'toolbar': True,
'shortcut': 'P_SHIFT_MOD1_MOD2',
},
{
'title': {'en': 'Copy in clipboard', 'es': 'Copiar al portapapeles'},
'argument': 'copy',
'context': 'calc,writer',
'icon': 'icon',
'toolbar': True,
'shortcut': 'C_SHIFT_MOD1_MOD2',
},
{
'title': {'en': 'Generate password...', 'es': 'Generar contraseña...'},
'argument': 'generate',
'context': 'calc,writer',
'icon': 'icon',
'toolbar': True,
'shortcut': 'G_SHIFT_MOD1_MOD2',
},
)
# ~ 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

7571
easymacro.py Normal file

File diff suppressed because it is too large Load Diff

BIN
files/ZAZPass_v0.1.0.oxt Normal file

Binary file not shown.

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

130
source/Addons.xcu Normal file
View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<oor:component-data xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:oor="http://openoffice.org/2001/registry" oor:name="Addons" oor:package="org.openoffice.Office">
<node oor:name="AddonUI">
<node oor:name="OfficeMenuBar">
<node oor:name="net.elmau.zaz.pass" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Zaz Passwords</value>
<value xml:lang="es">Zaz Contraseñas</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<node oor:name="Submenu">
<node oor:name="m0" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Insert password</value>
<value xml:lang="es">Insertar contraseña</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:net.elmau.zaz.pass?insert</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
<node oor:name="m1" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Copy in clipboard</value>
<value xml:lang="es">Copiar al portapapeles</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:net.elmau.zaz.pass?copy</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
<node oor:name="m2" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Generate password...</value>
<value xml:lang="es">Generar contraseña...</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:net.elmau.zaz.pass?generate</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
</node>
</node>
</node>
<node oor:name="OfficeToolBar">
<node oor:name="net.elmau.zaz.pass" oor:op="replace">
<node oor:name="t0" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Insert password</value>
<value xml:lang="es">Insertar contraseña</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:net.elmau.zaz.pass?insert</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
<node oor:name="t1" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Copy in clipboard</value>
<value xml:lang="es">Copiar al portapapeles</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:net.elmau.zaz.pass?copy</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
<node oor:name="t2" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en">Generate password...</value>
<value xml:lang="es">Generar contraseña...</value>
</prop>
<prop oor:name="Context" oor:type="xs:string">
<value>com.sun.star.sheet.SpreadsheetDocument,com.sun.star.text.TextDocument</value>
</prop>
<prop oor:name="URL" oor:type="xs:string">
<value>service:net.elmau.zaz.pass?generate</value>
</prop>
<prop oor:name="Target" oor:type="xs:string">
<value>_self</value>
</prop>
<prop oor:name="ImageIdentifier" oor:type="xs:string">
<value>%origin%/images/icon</value>
</prop>
</node>
</node>
</node>
</node>
</oor:component-data>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" manifest:version="1.2">
<manifest:file-entry manifest:full-path="ZAZPass.py" manifest:media-type="application/vnd.sun.star.uno-component;type=Python"/>
<manifest:file-entry manifest:full-path="Office/Accelerators.xcu" manifest:media-type="application/vnd.sun.star.configuration-data"/>
<manifest:file-entry manifest:full-path="Addons.xcu" manifest:media-type="application/vnd.sun.star.configuration-data"/>
</manifest:manifest>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<oor:component-data xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:oor="http://openoffice.org/2001/registry" oor:name="Accelerators" oor:package="org.openoffice.Office">
<node oor:name="PrimaryKeys">
<node oor:name="Modules">
<node oor:name="com.sun.star.sheet.SpreadsheetDocument">
<node oor:name="P_SHIFT_MOD1_MOD2" oor:op="fuse">
<prop oor:name="Command">
<value xml:lang="en-US">service:net.elmau.zaz.pass?insert</value>
</prop>
</node>
</node>
<node oor:name="com.sun.star.text.TextDocument">
<node oor:name="P_SHIFT_MOD1_MOD2" oor:op="fuse">
<prop oor:name="Command">
<value xml:lang="en-US">service:net.elmau.zaz.pass?insert</value>
</prop>
</node>
</node>
<node oor:name="com.sun.star.sheet.SpreadsheetDocument">
<node oor:name="C_SHIFT_MOD1_MOD2" oor:op="fuse">
<prop oor:name="Command">
<value xml:lang="en-US">service:net.elmau.zaz.pass?copy</value>
</prop>
</node>
</node>
<node oor:name="com.sun.star.text.TextDocument">
<node oor:name="C_SHIFT_MOD1_MOD2" oor:op="fuse">
<prop oor:name="Command">
<value xml:lang="en-US">service:net.elmau.zaz.pass?copy</value>
</prop>
</node>
</node>
<node oor:name="com.sun.star.sheet.SpreadsheetDocument">
<node oor:name="G_SHIFT_MOD1_MOD2" oor:op="fuse">
<prop oor:name="Command">
<value xml:lang="en-US">service:net.elmau.zaz.pass?generate</value>
</prop>
</node>
</node>
<node oor:name="com.sun.star.text.TextDocument">
<node oor:name="G_SHIFT_MOD1_MOD2" oor:op="fuse">
<prop oor:name="Command">
<value xml:lang="en-US">service:net.elmau.zaz.pass?generate</value>
</prop>
</node>
</node>
</node>
</node>
</oor:component-data>

22
source/ZAZPass.py Normal file
View File

@ -0,0 +1,22 @@
import uno
import unohelper
from com.sun.star.task import XJobExecutor
from zpass import main
ID_EXTENSION = 'net.elmau.zaz.pass'
SERVICE = ('com.sun.star.task.Job',)
class ZAZPass(unohelper.Base, XJobExecutor):
def __init__(self, ctx):
self.ctx = ctx
def trigger(self, args):
main(ID_EXTENSION, args, __file__)
return
g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation(ZAZPass, ID_EXTENSION, SERVICE)

26
source/description.xml Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<description xmlns="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:d="http://openoffice.org/extensions/description/2006">
<identifier value="net.elmau.zaz.pass"/>
<version value="0.1.0"/>
<display-name>
<name lang="en">My first extension</name>
<name lang="es">Mi primer extensión</name>
</display-name>
<extension-description>
<src lang="en" xlink:href="description/desc_en.txt"/>
<src lang="es" xlink:href="description/desc_es.txt"/>
</extension-description>
<icon>
<default xlink:href="images/zazpass.png"/>
</icon>
<publisher>
<name xlink:href="https://git.cuates.net/elmau" lang="en">El Mau</name>
<name xlink:href="https://git.cuates.net/elmau" lang="es">El Mau</name>
</publisher>
<registration>
<simple-license accept-by="user" suppress-on-update="true">
<license-text xlink:href="registration/license_en.txt" lang="en"/>
<license-text xlink:href="registration/license_es.txt" lang="es"/>
</simple-license>
</registration>
</description>

View File

@ -0,0 +1 @@
My great extension

View File

@ -0,0 +1 @@
Mi gran extensión

4
source/images/close.svg Executable file
View File

@ -0,0 +1,4 @@
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.17218 14.8284L12.0006 12M14.829 9.17157L12.0006 12M12.0006 12L9.17218 9.17157M12.0006 12L14.829 14.8284" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 8C7.5 14.5 16.5 14.5 19.5 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.8162 11.3175L19.5 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 12.875V16.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.18383 11.3175L4.5 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C19.1114 14.991 15.7183 18 12 18C8.2817 18 4.88856 14.991 3 12C5.29855 9.15825 7.99163 6 12 6C16.0084 6 18.7015 9.1582 21 12Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

4
source/images/insert.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 11L12 14L20 6" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C12.9473 4 13.8561 4.16464 14.6994 4.46686" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

6
source/images/new.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.1679 8C19.6247 4.46819 16.1006 2 11.9999 2C6.81459 2 2.55104 5.94668 2.04932 11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 8H21.4C21.7314 8 22 7.73137 22 7.4V3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.88146 16C4.42458 19.5318 7.94874 22 12.0494 22C17.2347 22 21.4983 18.0533 22 13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.04932 16H2.64932C2.31795 16 2.04932 16.2686 2.04932 16.6V21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 717 B

BIN
source/images/zazpass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because it is too large Load Diff

331
source/pythonpath/stats.py Normal file
View File

@ -0,0 +1,331 @@
#!/usr/bin/env python3
# ~ Thanks:
# ~ https://github.com/kolypto/py-password-strength/blob/master/password_strength/stats.py
import re
import unicodedata
from collections import Counter
from functools import wraps
from math import log
def cached_property(f):
""" Property that will replace itself with a calculated value """
name = '__' + f.__name__
@wraps(f)
def wrapper(self):
if not hasattr(self, name):
setattr(self, name, f(self))
return getattr(self, name)
return property(wrapper)
class PasswordStats(object):
""" PasswordStats allows to calculate statistics on a password.
It considers a password as a unicode string, and all statistics are unicode-based.
"""
def __init__(self, password):
self.password = password
#region Statistics
@cached_property
def alphabet(self):
""" Get alphabet: set of used characters
:rtype: set
"""
return set(self.password)
@cached_property
def alphabet_cardinality(self):
""" Get alphabet cardinality: alphabet length
:rtype: int
"""
return len(self.alphabet)
@cached_property
def char_categories_detailed(self):
""" Character count per unicode category, detailed format.
See: http://www.unicode.org/reports/tr44/#GC_Values_Table
:returns: Counter( unicode-character-category: count )
:rtype: collections.Counter
"""
return Counter(map(unicodedata.category, self.password))
@cached_property
def char_categories(self):
""" Character count per top-level category
The following top-level categories are defined:
- L: letter
- M: Mark
- N: Number
- P: Punctuation
- S: Symbol
- Z: Separator
- C: Other
:return: Counter(unicode-character-category: count }
:rtype: collections.Counter
"""
c = Counter()
for cat, n in self.char_categories_detailed.items():
c[cat[0]] += n
return c
#endregion
#region Counters
@cached_property
def length(self):
""" Get password length
:rtype: int
"""
return len(self.password)
@cached_property
def letters(self):
""" Count all letters
:rtype: int
"""
return self.char_categories['L']
@cached_property
def letters_uppercase(self):
""" Count uppercase letters
:rtype: int
"""
return self.char_categories_detailed['Lu']
@cached_property
def letters_lowercase(self):
""" Count lowercase letters
:rtype: int
"""
return self.char_categories_detailed['Ll']
@cached_property
def numbers(self):
""" Count numbers
:rtype: int
"""
return self.char_categories['N']
def count(self, *categories):
""" Count characters of the specified classes only
:param categories: Character categories to count
:type categories: Iterable
:rtype: int
"""
return sum([int(cat_n[0] in categories) * cat_n[1] for cat_n in list(self.char_categories.items())])
def count_except(self, *categories):
""" Count characters of all classes except the specified ones
:param categories: Character categories to exclude from count
:type categories: Iterable
:rtype: int
"""
return sum([int(cat_n1[0] not in categories) * cat_n1[1] for cat_n1 in list(self.char_categories.items())])
@cached_property
def special_characters(self):
""" Count special characters
Special characters is everything that's not a letter or a number
:rtype: int
"""
return self.count_except('L', 'N')
#region Security
@cached_property
def combinations(self):
""" The number of possible combinations with the current alphabet
:rtype: long
"""
return self.alphabet_cardinality ** self.length
@cached_property
def entropy_bits(self):
""" Get information entropy bits: log2 of the number of possible passwords
https://en.wikipedia.org/wiki/Password_strength
:rtype: float
"""
return self.length * log(self.alphabet_cardinality, 2)
@cached_property
def entropy_density(self):
""" Get information entropy density factor, ranged {0 .. 1}.
This is ratio of entropy_bits() to max bits a password of this length could have.
E.g. if all characters are unique -- then it's 1.0.
If half of the characters are reused once -- then it's 0.5.
:rtype: float
"""
# Simplifying:
# entropy_bits / (length * log(length, 2)) =
# = log(alphabet_cardinality, 2) / log(length, 2) =
# = log(alphabet_cardinality, length)
return log(self.alphabet_cardinality, self.length)
def strength(self, weak_bits=30):
""" Get password strength as a number normalized to range {0 .. 1}.
Normalization is done in the following fashion:
1. If entropy_bits <= weak_bits -- linear in range{0.0 .. 0.33} (weak)
2. If entropy_bits <= weak_bits*2 -- almost linear in range{0.33 .. 0.66} (medium)
3. If entropy_bits > weak_bits*3 -- asymptotic towards 1.0 (strong)
:param weak_bits: Minimum entropy bits a medium password should have.
:type weak_bits: int
:return: Normalized password strength:
* <0.33 is WEAK
* <0.66 is MEDIUM
* >0.66 is STRONG
:rtype: float
"""
WEAK_MAX = 0.333333333
if self.entropy_bits <= weak_bits:
return WEAK_MAX * self.entropy_bits / weak_bits
HARD_BITS = weak_bits*3
HARD_VAL = 0.950
# Here, we want a function that:
# 1. f(x)=0.333 at x=weak_bits
# 2. f(x)=0.950 at x=weak_bits*3 (great estimation for a perfect password)
# 3. f(x) is almost linear in range{weak_bits .. weak_bits*2}: doubling the bits should double the strength
# 4. f(x) has an asymptote of 1.0 (normalization)
# First, the function:
# f(x) = 1 - (1-WEAK_MAX)*2^( -k*x)
# Now, the equation:
# f(HARD_BITS) = HARD_VAL
# 1 - (1-WEAK_MAX)*2^( -k*HARD_BITS) = HARD_VAL
# 2^( -k*HARD_BITS) = (1 - HARD_VAL) / (1-WEAK_MAX)
# k = -log2((1 - HARD_VAL) / (1-WEAK_MAX)) / HARD_BITS
k = -log((1 - HARD_VAL) / (1-WEAK_MAX), 2) / HARD_BITS
f = lambda x: 1 - (1-WEAK_MAX)*pow(2, -k*x)
return f(self.entropy_bits - weak_bits) # with offset
#endregion
#region Detectors
_repeated_patterns_rex = re.compile(r'((.+?)\2+)', re.UNICODE | re.DOTALL | re.IGNORECASE)
@cached_property
def repeated_patterns_length(self):
""" Detect and return the length of repeated patterns.
You will probably be comparing it with the length of the password itself and ban if it's longer than 10%
:rtype: int
"""
length = 0
for substring, pattern in self._repeated_patterns_rex.findall(self.password):
length += len(substring)
return length
_sequences = (
'abcdefghijklmnopqrstuvwxyz' # Alphabet
'qwertyuiopasdfghjklzxcvbnm' # Keyboard
'~!@#$%^&*()_+-=' # Keyboard special, top row
'01234567890' # Numbers
)
_sequences = _sequences + _sequences[::-1] # reversed
@cached_property
def sequences_length(self):
""" Detect and return the length of used sequences:
- Alphabet letters: abcd...
- Keyboard letters: qwerty, etc
- Keyboard special characters in the top row: ~!@#$%^&*()_+
- Numbers: 0123456
:return: Total length of character sequences that are subsets of the common sequences
:rtype: int
"""
# FIXME: Optimize this. I'm sure there is a better way!...
sequences_length = 0
# Iterate through the string, with manual variable (to allow skips)
i = 0
while i < len(self.password):
# Slice (since we use it often)
password = self.password[i:]
# Iterate over sequences to find longest common prefix
j = -1
common_length = 1
while True:
# Detect the first match with the current character
# A character may appear multiple times
j = self._sequences.find(password[0], j+1)
if j == -1:
break
# Find the longest common prefix
common_here = ''
for a, b in zip(password, self._sequences[j:]):
if a != b: break
else: common_here += a
# It it's longer than previous discoveries -- store it
common_length = max(common_length, len(common_here))
# Repeated sequence?
if common_length > 2:
sequences_length += common_length
# Next: skip to the end of the detected sequence
i += common_length
return sequences_length
@cached_property
def weakness_factor(self):
""" Get weakness factor as a float in range {0 .. 1}
This detects the portion of the string that contains:
* repeated patterns
* sequences
E.g. a value of 1.0 means the whole string is weak, and 0.5 means half of the string is weak.
Typical usage:
password_strength = (1 - weakness_factor) * strength
:return: Weakness factor
:rtype: float
"""
return min(1.0, (self.repeated_patterns_length + self.sequences_length) / self.length)

348
source/pythonpath/zpass.py Normal file
View File

@ -0,0 +1,348 @@
#!/usr/bin/env python3
import string
import random
import easymacro as app
from stats import PasswordStats
_ = None
PREFIX = 'zazpass'
DEFAULT_LENGTH = 25
DEFAULT_CHARACTERS = (
string.ascii_uppercase +
string.ascii_lowercase +
string.digits +
string.punctuation)
def main(id_extension, args, path_locales):
global _
if args == 'insert':
_password_insert()
return
if args == 'copy':
_password_copy()
return
_ = app.install_locales(path_locales)
_password_generate(id_extension)
return
def _from_config():
characters = ''
config = app.get_config('setting', PREFIX)
length = config.get('length', DEFAULT_LENGTH)
if config:
if config['letters']:
characters += string.ascii_uppercase
if config['letters2']:
characters += string.ascii_lowercase
if config['digits']:
characters += string.digits
if config['punctuation']:
characters += string.punctuation
else:
characters = DEFAULT_CHARACTERS
return list(characters), length
def _from_controls(dialog):
characters = ''
length = int(dialog.txt_length.value)
if dialog.chk_letters.value:
characters += string.ascii_uppercase
if dialog.chk_letters2.value:
characters += string.ascii_lowercase
if dialog.chk_digits.value:
characters += string.digits
if dialog.chk_punctuation.value:
characters += string.punctuation
if not characters:
dialog.chk_letters.value = True
characters = string.ascii_uppercase
return list(characters), length
def _get_data(dialog):
if dialog is None:
characters, lenght = _from_config()
else:
characters, lenght = _from_controls(dialog)
return characters, lenght
def _generate(dialog=None):
characters, length = _get_data(dialog)
random.shuffle(characters)
password = [random.choice(characters) for i in range(length)]
random.shuffle(password)
password = ''.join(password)
return password
def _password_insert():
for cell in app.selection:
cell.str = _generate()
return
def _password_copy():
app.clipboard.set(_generate())
return
def _password_generate(id_extension):
config = app.get_config('setting', prefix=PREFIX)
dialog = _create_dialog(id_extension)
length = config.get('length', DEFAULT_LENGTH)
letters = True
letters2 = True
digits = True
punctuation = True
if config:
letters = config['letters']
letters2 = config['letters2']
digits = config['digits']
punctuation = config['punctuation']
dialog.chk_letters.value = letters
dialog.chk_letters2.value = letters2
dialog.chk_digits.value = digits
dialog.chk_punctuation.value = punctuation
dialog.txt_length.value = length
dialog.txt_password.value = _generate(dialog)
dialog.open()
return
class Controllers(object):
def __init__(self, dialog):
self.d = dialog
def cmd_close_action(self, event):
self.d.close()
return
def cmd_switch_action(self, event):
char = self.d.txt_password.echochar
name_image = 'eye-close.svg'
if char == '*':
self.d.txt_password.echochar = ''
name_image = 'eye-open.svg'
else:
self.d.txt_password.echochar = '*'
path_image = app.paths.join(self.d.path_images, name_image)
self.d.cmd_switch.image = path_image
self.d.txt_password.set_focus()
return
def cmd_insert_action(self, event):
for cell in app.selection:
cell.str = self.d.txt_password.value
self.d.close()
return
def _save_config(self):
data = dict(
length = self.d.txt_length.int,
letters = self.d.chk_letters.value,
letters2 = self.d.chk_letters2.value,
digits = self.d.chk_digits.value,
punctuation = self.d.chk_punctuation.value,
)
app.set_config('setting', data, PREFIX)
return
def _new_password(self, save=True):
stats = PasswordStats(_generate(self.d))
self.d.txt_password.value = stats.password
if save:
self._save_config()
return
def cmd_new_action(self, event):
self._new_password(False)
return
def txt_length_after_click(self, event):
self._new_password()
return
def chk_letters_after_click(self, event):
self._new_password()
return
def chk_letters2_after_click(self, event):
self._new_password()
return
def chk_digits_after_click(self, event):
self._new_password()
return
def chk_punctuation_after_click(self, event):
self._new_password()
return
@app.catch_exception
def _create_dialog(id_extension):
BUTTON_WH = 16
CHK_WIDTH = 25
attr = dict(
Name = 'Dialog',
Title = _('Generate Password'),
Width = 200,
Height = 100,
)
dialog = app.create_dialog(attr)
dialog.id = id_extension
dialog.events = Controllers
attr = dict(
Type = 'Text',
Name = 'txt_password',
Width = 150,
Height = 12,
X = 5,
Y = 10,
EchoChar = ord('*'),
)
dialog.add_control(attr)
attr = dict(
Type = 'Button',
Name = 'cmd_switch',
Width = BUTTON_WH,
Height = BUTTON_WH,
ImageURL = 'eye-close.svg',
FocusOnClick = True,
)
dialog.add_control(attr)
attr = dict(
Type = 'Button',
Name = 'cmd_new',
Width = BUTTON_WH,
Height = BUTTON_WH,
ImageURL = 'new.svg',
)
dialog.add_control(attr)
attr = dict(
Type = 'Label',
Name = 'lbl_title_length',
Label = _('Length: '),
Width = 50,
Height = BUTTON_WH,
Border = 1,
Align = 2,
VerticalAlign = 1,
)
dialog.add_control(attr)
attr = dict(
Type = 'Numeric',
Name = 'txt_length',
Width = 50,
Height = BUTTON_WH,
DecimalAccuracy = 0,
Spin = True,
Value = 25,
ValueStep = 1,
ValueMin = 10,
ValueMax = 100,
)
dialog.add_control(attr)
attr = dict(
Type = 'CheckBox',
Name = 'chk_letters',
Label = 'A-Z',
Width = CHK_WIDTH,
Height = BUTTON_WH,
)
dialog.add_control(attr)
attr = dict(
Type = 'CheckBox',
Name = 'chk_letters2',
Label = 'a-z',
Width = CHK_WIDTH,
Height = BUTTON_WH,
)
dialog.add_control(attr)
attr = dict(
Type = 'CheckBox',
Name = 'chk_digits',
Label = '0-9',
Width = CHK_WIDTH,
Height = BUTTON_WH,
)
dialog.add_control(attr)
attr = dict(
Type = 'CheckBox',
Name = 'chk_punctuation',
Label = string.punctuation,
Width = CHK_WIDTH * 4,
Height = BUTTON_WH,
)
dialog.add_control(attr)
attr = dict(
Type = 'Button',
Name = 'cmd_insert',
Label = _('~Insert'),
Width = 70,
Height = BUTTON_WH,
ImageURL = 'insert.svg',
ImagePosition = 1,
)
dialog.add_control(attr)
attr = dict(
Type = 'Button',
Name = 'cmd_close',
Label = _('~Close'),
Width = 70,
Height = BUTTON_WH,
ImageURL = 'close.svg',
ImagePosition = 1,
)
dialog.add_control(attr)
dialog.cmd_switch.move(dialog.txt_password, x=3, y=-2)
dialog.cmd_new.move(dialog.cmd_switch, x=3, y=0)
dialog.lbl_title_length.move(dialog.txt_password)
dialog.txt_length.move(dialog.lbl_title_length, x=3, y=0)
dialog.chk_letters.move(dialog.lbl_title_length)
dialog.chk_letters2.move(dialog.chk_letters, x=3, y=0)
dialog.chk_digits.move(dialog.chk_letters2, x=3, y=0)
dialog.chk_punctuation.move(dialog.chk_digits, x=3, y=0)
dialog.center(dialog.cmd_insert, y=-5)
dialog.center(dialog.cmd_close, y=-5)
dialog.center((dialog.cmd_close, dialog.cmd_insert))
return dialog

View File

@ -0,0 +1,14 @@
This file is part of ZAZPass.
ZAZPass 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.
ZAZPass 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 ZAZPass. If not, see <https://www.gnu.org/licenses/>.

View File

@ -0,0 +1,14 @@
This file is part of ZAZPass.
ZAZPass 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.
ZAZPass 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 ZAZPass. If not, see <https://www.gnu.org/licenses/>.

823
zaz.py Executable file
View File

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