"""A file manager for generic XML files."""
# pylint: disable = too-many-lines
from __future__ import annotations
from os.path import dirname
from re import sub as re_sub
from json import loads
from lxml import etree
from pygments import highlight
from pygments.lexers.html import XmlLexer
from pygments.formatters.html import HtmlFormatter
from pyramid.config import Configurator
from pyramid.request import Request
from chrysalio.lib.utils import tounicode
from chrysalio.lib.form import Form, get_action
from chrysalio.lib.xml import load_xml2, validate_xml, load_xslt
from chrysalio.lib.xml import relaxng4validation
from chrysalio.helpers.literal import Literal
from chrysalio.includes.themes import theme_static_prefix
from ciowarehouse2.lib.utils import apply_regex
from ciowarehouse2.lib.ciotype import CioType
from ciowarehouse2.lib.ciopath import CioPath
from ciowarehouse2.lib.warehouse import Warehouse
from ciowarehouse2.lib.manager import LAYOUT_VIEW_PT, LAYOUT_EDIT_PT
from ciowarehouse2.lib.manager import CHRYSALIO_JS, Manager
from ...lib.i18n import _, translate
from ...lib.utils import special_protect, special_unprotect
MEDIA_HTTP_NOTFOUND = {
'image': '/cioxml2/images/notfound.jpg',
'audio': '/cioxml2/audios/notfound.ogg',
'video': '/cioxml2/videos/notfound.mp4'
}
IMAGE_WEB_EXTENSIONS = ('.svg', '.webp', '.png', '.jpg', '.jpeg', '.gif')
MEDIA_EXTENSIONS = {
'image': ('', ) + IMAGE_WEB_EXTENSIONS +
('.tif', '.tiff', '.eps', '.bmp', '.psd', '.ai'),
'audio': ('', '.ogg', '.wav', '.mp3', '.m4a'),
'video': ('', '.ogv', '.webm', '.mp4')
}
ATTRIBUTE_NS_HTML = ' xmlns="http://www.w3.org/1999/xhtml"'
EDIT_CXE_FRAME = """
<div class="cxeZone">
<div id="cxeToolbar"><div id="cxeMenu"></div></div>
<div id="cxeWrapper">
<div id="cxeViewZone"><div id="cxeView" class="{class_}"></div></div>
<div id="cxeTrail"></div>
</div>
<textarea id="cxeSource" name="content"{params}>{content}</textarea>
{aside}
</div>
"""
EDIT_XML_FRAME = '<textarea id="cmCode" name="content">{0}</textarea>'
MANAGER_CSS = ('/cioxml2/css/manager.css', )
MANAGER_XML_CSS = ('/cioxml2/css/manager_xml.css', )
CODEMIRROR_CSS = (
'/ciowarehouse2/css/codemirror.css', '/ciowarehouse2/css/show-hint.css',
'/cioxml2/css/manager_xml.css')
CODEMIRROR_JS = ('/cioxml2/js/cm4xml.js', )
KATEX_CSS = ('{katex_css}', )
KATEX_CSS_CDN = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css'
KATEX_CSS_LOCAL = '/ciokatex/css/katex.css'
KATEX_JS = ('{katex_js}', )
KATEX_JS_CDN = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js'
KATEX_JS_LOCAL = '/ciokatex/js/katex.js'
CIOKATEX_JS = ('/cioxml2/js/ciokatex.js', )
MATHLIVE_CSS = ('{mathlive_css}', )
MATHLIVE_CSS_CDN = 'https://unpkg.com/mathlive/dist/mathlive-static.css'
MATHLIVE_CSS_LOCAL = '/ciomathlive/mathlive/mathlive.css'
MATHLIVE_JS = ('{compute_engine_js}', '{mathlive_js}')
MATHLIVE_JS_CDN = 'https://unpkg.com/mathlive'
MATHLIVE_JS_LOCAL = '/ciomathlive/mathlive/mathlive.js'
COMPUTE_ENGINE_JS_CDN = 'https://unpkg.com/@cortex-js/compute-engine'
COMPUTE_ENGINE_JS_LOCAL = '/ciomathlive/mathlive/compute-engine.js'
# =============================================================================
[docs]
def includeme(configurator: Configurator):
"""Function to include a CioWarehouse2 manager.
:type configurator: pyramid.config.Configurator
:param configurator:
Object used to do configuration declaration within the application.
"""
Manager.register(configurator, ManagerXml)
# =============================================================================
[docs]
class ManagerXml(Manager):
"""Class to manage a generic XML file.
``viewings`` and ``editings`` dictionaries have the following additional
keys:
* ``'xsl'``: XSL file to transform the XML file
* ``'image_paths'``: list of paths to dynamically find images
* ``'image_extensions'``: list of authorized extensions for images
* ``'audio_paths'``: list of paths to dynamically find audios
* ``'audio_extensions'``: list of authorized extensions for audio
* ``'video_paths'``: list of paths to dynamically find videos
* ``'video_extensions'``: list of authorized extensions for videos
The three last fields can contain the following tag:
* ``{directory}``: path of the directory containing the input file
"""
ciotype = CioType('xml')
uid = 'xml'
label = _('Generic XML file handling')
viewings: tuple = ( # yapf: disable
{'name': 'xml',
'label': _('XML'),
'template': LAYOUT_VIEW_PT,
'css': MANAGER_XML_CSS,
'js': None,
'xsl': None,
'image_paths': (), 'image_extensions': (),
'audio_paths': (), 'audio_extensions': (),
'video_paths': (), 'video_extensions': ()}, )
editings: tuple = ( # yapf: disable
{'name': 'xml',
'label': _('XML'),
'template': LAYOUT_EDIT_PT,
'css': CODEMIRROR_CSS,
'js': CHRYSALIO_JS + CODEMIRROR_JS}, )
relaxng: dict | None = None
class_xpath: str | None = None
_home = dirname(__file__)
_xslt: dict = {}
# -------------------------------------------------------------------------
[docs]
def view(
self,
request: Request,
warehouse: Warehouse,
ciopath: CioPath,
ts_factory=None) -> str | None:
"""Return a string containing HTML to display the file.
See: :meth:`ciowarehouse2.lib.manager.Manager.view`
"""
# Create content
viewing = self.current_rendering(request, warehouse, 'viewing')
if not ciopath or viewing is None:
return None
if viewing.get('xsl'):
self.install(self._develop)
if self._develop:
self._xslt = {} # pragma: nocover
content, class_ = self.xml2html(
request, warehouse, ciopath, viewing)[:2]
if content is not None:
content = content.replace(ATTRIBUTE_NS_HTML, '')
content = self._transform_pi(
warehouse, viewing, content, request)
else:
try:
abs_path = ciopath.absolute_path(warehouse.root)
with open(abs_path, 'r', encoding='utf8') as hdl:
content = hdl.read()
except (OSError, IOError, UnicodeError):
return None
content = highlight(content, XmlLexer(), HtmlFormatter())
class_ = None
if content is None:
return None
# Create HTML
html = self._chameleon_render(
request, warehouse, ciopath, viewing, ts_factory or _, {
'class': class_,
'rendering_num':
request.session['managers'][self.uid]['viewing'],
'content': Literal(content)
})
return html
# -------------------------------------------------------------------------
[docs]
def edit(
self,
request: Request,
warehouse: Warehouse,
ciopath: CioPath,
ts_factory=None) -> str | None:
"""Return a string containing HTML to edit the file.
See: :meth:`ciowarehouse2.lib.manager.Manager.edit`
"""
# pylint: disable = too-many-locals
# Rendering
editing = self.current_rendering(request, warehouse, 'editing')
if not ciopath or not editing or (
editing.get('only4groups') is not None
and not (editing['only4groups']
& set(request.session['user']['groups']))):
return None
# Lock file
locked, locker = self.file_lock(request, warehouse, ciopath)
if not locked:
request.session.flash(translate( # yapf: disable
_('The file is locked by "${n}"!', {'n': locker}),
request=request), 'alert')
return None
# Form, frame, class and original content
form: Form | None = None
frame = ''
class_: str | None = None
original: etree.ElementTree | None = None
if editing['name'].startswith('cxe'):
form, frame, class_ = self.edit_cxe(
request, warehouse, ciopath, editing)
elif editing.get('xsl'):
form, frame, class_, original = self.edit_mask(
request, warehouse, ciopath, editing)
frame = self._transform_pi(warehouse, editing, frame, request)
else:
form, frame = self.edit_xml(request, warehouse, ciopath)
if form is None:
self.file_unlock(request, warehouse, ciopath)
return None
# Action
action = get_action(request)[0]
message = ''
queue = ''
if action in ('saq!', 'sav!') and form.validate():
error = self.save(
request, warehouse, ciopath, editing, form.values,
action == 'sav!', original)
if error != '':
message = _('"${f}" successfully saved.', {'f': ciopath}) \
if error is None else error
queue = 'alert' if error is not None else ''
if action == 'saq!' and not error:
if message:
request.session.flash(
translate(message, request=request), queue)
return None
# HTML
html = self._chameleon_render(
request, warehouse, ciopath, editing, ts_factory or _, {
'form': form,
'class': class_,
'rendering_num':
request.session['managers'][self.uid]['editing'],
'content': Literal(frame)
})
if html is None:
self.file_unlock(request, warehouse, ciopath) # pragma: nocover
# Message
if message:
request.session.flash(translate(message, request=request), queue)
return html
# -------------------------------------------------------------------------
[docs]
def edit_cxe(
self, request: Request, warehouse: Warehouse, ciopath: CioPath,
editing: dict) -> tuple[Form | None, str, str | None]:
"""Return a string containing HTML to edit in a WYSIWYM manner.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` to the file.
:param dict editing:
Dictionary representing the editing.
:rtype: tuple
:return:
A tuple such as ``(form, frame, class)``.
"""
# Content and aside
self.install(self._develop)
if self._develop:
self._xslt = {} # pragma: nocover
if 'content' in request.POST:
content = self._save_cxe_transform(
request, editing, request.POST['content'])
else:
try:
abs_path = ciopath.absolute_path(warehouse.root)
with open(abs_path, 'r', encoding='utf8') as hdl:
content = hdl.read()
except (OSError, IOError, UnicodeError):
return None, '', None
aside = self._edit_cxe_aside(
request, warehouse, ciopath, editing, content)
content, class_ = self._edit_cxe_transform(request, editing, content)
# Debug
# print('-' * 80, 'IN')
# print(content)
# Parameters
ciotype_route = self.ciotype.route()
params = \
'data-input_warehouse="{warehouse}" '\
'data-input_directory="{directory}/" '\
'data-route_preview="{preview}" '\
'data-route_image="{image}" '\
'data-route_audio="{audio}" '\
'data-route_video="{video}" '\
'{i18n}'.format(
warehouse=ciopath.wid,
directory=ciopath.directory() or '.',
preview=request.route_path(
'file_preview', ciotype=ciotype_route, ciopath=''),
image=request.route_path(
'media_download', rendering='editing', media_type='image',
ciotype=ciotype_route, ciopath=''),
audio=request.route_path(
'media_download', rendering='editing', media_type='audio',
ciotype=ciotype_route, ciopath=''),
video=request.route_path(
'media_download', rendering='editing', media_type='video',
ciotype=ciotype_route, ciopath=''),
i18n=self._edit_cxe_i18n(request))
return \
Form(request), \
EDIT_CXE_FRAME.format(
class_=editing['class']
if 'class' in editing else self.uid.replace('xml_', ''),
params=params,
content=special_protect(
re_sub('<\\?xml[^>]+>', '', content)),
aside=special_protect(
re_sub('<\\?xml[^>]+>', '', aside))), \
class_
# -------------------------------------------------------------------------
def _edit_cxe_transform(
self, request: Request, editing: dict,
content: str) -> tuple[str, str | None]:
"""Possibly convert the content before use and return it as a string.
:type request: pyramid.request.Request
:param request:
Current request.
:param dict editing:
Dictionary representing the editing.
:param content:
Content of the file
:rtype: tuple
:return:
A tuple such as ``(content, class_)``
"""
# Regex
if 'regex' in editing and editing['regex'][0]:
content = apply_regex(
request, self.abspath_from_home(editing['regex'][0]), content)
# XSL
uid = f'{self.uid}:cxe_in'
if uid not in self._xslt:
try:
self._xslt[uid] = etree.XSLT(etree.parse(
self.abspath_from_home(editing['xsl'][0])))\
if 'xsl' in editing and editing['xsl'][0] else None
except (IOError, etree.XSLTParseError,
etree.XMLSyntaxError) as err: # pragma: nocover
self._log_error(err, request)
self._xslt[uid] = None
if self._xslt[uid] is None:
return content, None
tree, error = load_xml2('content.xml', data=content)
if error is not None or tree is None:
self._log_error(error, request)
return content, None
class_ = None
if self.class_xpath:
class_ = tree.xpath(self.class_xpath)
class_ = class_ if class_ else None
try:
tree = self._xslt[uid](tree)
except (etree.XSLTApplyError, TypeError) as err: # pragma: nocover
self._log_error(err, request)
return content, class_
return str(tree), class_
# -------------------------------------------------------------------------
def _edit_cxe_aside(
self, request: Request, warehouse: Warehouse, ciopath: CioPath,
editing: dict, content: str) -> str:
"""Return a possibly string containing additional information.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: .lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` to the file.
:param dict editing:
Dictionary representing the editing.
:param content:
Content of the file.
:rtype: str
"""
# pylint: disable = unused-argument
return ''
# -------------------------------------------------------------------------
@classmethod
def _edit_cxe_i18n(cls, request: Request) -> str:
"""Return a string of data-i18n_* attributes for translations in
CioXmlEditor.
:type request: pyramid.request.Request
:param request:
Current request.
:rtype: str
"""
return 'data-i18n_select_parent="{select_parent}"'\
' data-i18n_undo="{undo}"'\
' data-i18n_redo="{redo}"'\
' data-i18n_lift="{lift}"'\
' data-i18n_join_down="{join_down}"'\
' data-i18n_join_up="{join_up}"'\
' data-i18n_ok="{ok}"'\
' data-i18n_cancel="{cancel}"'\
' data-i18n_close="{close}"'\
' data-i18n_required_value="{required}"'\
' data-i18n_excluded_value="{excluded}"'\
' data-i18n_format_not_valid="{not_valid}"'\
' data-i18n_key="{key}"'\
' data-i18n_value="{value}"'\
' data-i18n_click4keyboard="{click4keyboard}"'\
' data-i18n_value="{value}"'\
' data-i18n_dblclick2edit="{dblclick2edit}"'.format(
select_parent=translate(
_('Parent node (Esc)'), request=request),
undo=translate(
_('Undoing last change (Ctrl-z)'), request=request),
redo=translate(
_('Redoing last undone change (Shift-Ctrl-z)'),
request=request),
lift=translate(_(
'Removal of a level (Ctrl-<)'), request=request),
join_down=translate(
_('Merging downwards (Alt-↓)'), request=request),
join_up=translate(
_('Merging upwards (Alt-↑)'), request=request),
ok=translate(_('OK'), request=request),
cancel=translate(_('Cancel'), request=request),
close=translate(_('Close the window'), request=request),
required=translate(_('Required value'), request=request),
excluded=translate(_('Excluded value'), request=request),
not_valid=translate(_('Format not valid'), request=request),
key=translate(_('Key'), request=request),
value=translate(_('Value'), request=request),
click4keyboard=translate(
_('Click to open/close the keyboard'), request=request),
dblclick2edit=translate(
_('Double click to edit'), request=request))
# -------------------------------------------------------------------------
[docs]
def edit_mask(
self,
request: Request,
warehouse: Warehouse,
ciopath: CioPath,
editing: dict,
tree: etree.ElementTree | None = None,
in_panel: bool = False
) -> tuple[Form | None, str, str | None, etree.ElementTree | None]:
"""Return a string containing HTML to edit the file with a mask.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` to the file.
:param dict editing:
Dictionary representing the editing.
:type tree: lxml.etree.ElementTree
:param tree: (optional)
Content of the file.
:param bool in_panel: (default=False)
``True`` if it is displayed in a panel.
:rtype: tuple
:return:
A tuple such as ``(form, frame, class, original)``.
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
# Frame and orginal content
self.install(self._develop)
if self._develop:
self._xslt = {} # pragma: nocover
frame, class_, tree = self.xml2html(
request, warehouse, ciopath, editing, 'frame', tree, in_panel)
if frame is None:
self._log_error(
translate(_('Incorrect XSL for frame'), request=request),
request)
return None, '', class_, tree
frame = frame.replace(ATTRIBUTE_NS_HTML, '')
# Values
html = self.xml2html(
request, warehouse, ciopath, editing, 'values', tree, in_panel)[0]
if html is None:
self._log_error(
translate(_('Incorrect XSL for values'), request=request),
request)
return None, '', class_, tree
try:
values = loads(html.replace('\n', '\\n'))
except (TypeError, ValueError):
self._log_error(
translate(_('Incorrect XSL for values'), request=request),
request)
return None, '', class_, tree
# Form
form, frame = self._edit_mask_form(request, values, frame, in_panel)
return form, frame, class_, tree
# -------------------------------------------------------------------------
@classmethod
def _edit_mask_form(
cls, request: Request, values: dict, frame: str,
in_panel: bool) -> tuple[Form, str]:
"""Return a string containing HTML to edit the file with a mask.
:type request: pyramid.request.Request
:param request:
Current request.
:param dict values:
Values for fields.
:param str frame:
Frame being processed.
:param bool in_panel:
``True`` if it is displayed in a panel.
:rtype: tuple
:return:
A tuple such as ``(form, frame)``.
"""
# Defaults
defaults = {}
for field in values:
field_id, input_type = field.split('@')
if input_type == 'checkbox':
defaults[field_id] = values[field]
elif input_type != 'options':
values[field] = special_protect(values[field])
defaults[field_id] = values[field].strip() \
if '\n' not in values[field] else '\n'.join([
k.strip() for k in values[field].split('\n')]).rstrip()
# Form
form = Form(request, defaults=defaults, force_defaults=in_panel)
if frame is None:
return form, ''
form_input = ''
for field in values:
field_id, input_type = field.split('@')
if input_type == 'hidden':
form_input = form.hidden(field_id)
elif input_type == 'checkbox':
form_input = form.custom_checkbox(field_id)
elif input_type == 'number':
form_input = form.text(field_id, type='number')
elif input_type == 'textarea':
form_input = form.textarea(field_id)
elif input_type == 'select' and '{0}@options'.format(
field_id) in values:
form_input = form.select(
field_id, None, values['{0}@options'.format(field_id)])
elif input_type != 'options':
form_input = form.text(field_id)
if input_type != 'options':
frame = frame.replace(f'__{field}__', tounicode(form_input))
return form, frame
# -------------------------------------------------------------------------
[docs]
@classmethod
def edit_xml(
cls,
request: Request,
warehouse: Warehouse,
ciopath: CioPath,
content: str | None = None) -> tuple[Form, str]:
"""Return a string containing HTML to edit the file as XML.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: .lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:param str path:
Relative path to the file.
:param content: (optional)
Content of the file.
:rtype: tuple
:return:
A tuple such as ``(form, frame)``.
"""
if 'content' in request.POST:
content = request.POST['content']
if content is None:
try:
abs_path = ciopath.absolute_path(warehouse.root)
with open(abs_path, 'r', encoding='utf8') as hdl:
content = hdl.read()
except (OSError, IOError, UnicodeError): # pragma: nocover
return None, ''
return \
Form(request), \
EDIT_XML_FRAME.format(
content.replace('<', '&lt;').replace(' ', '‧'))
# -------------------------------------------------------------------------
[docs]
def save(
self,
request: Request,
warehouse: Warehouse,
ciopath: CioPath,
editing: dict,
values: dict,
go_on: bool,
original: etree.ElementTree | None = None) -> str | None:
"""Save the XML file.
See: :meth:`ciowarehouse2.lib.manager.Manager.save`
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
if editing['name'].startswith('cxe'):
return self._save_cxe(
request, warehouse, ciopath, editing, values, go_on)
if editing['name'] == 'mask':
return self._save_mask(
request, warehouse, ciopath, values, go_on, original)
if editing['name'] == 'xml':
return self._save_xml(request, warehouse, ciopath, values, go_on)
self.file_unlock(request, warehouse, ciopath)
return translate(_('Unknown editing mode'), request=request)
# -------------------------------------------------------------------------
def _save_cxe(
self, request: Request, warehouse: Warehouse, ciopath: CioPath,
editing: dict, values: dict, go_on: bool) -> str | None:
"""Update XML in a WYSIWYM manner.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` of the file.
:param dict editing:
Dictionary representing the editing.
:param dict values:
Values of the form.
:param bool go_on:
``True`` if the modification continues after saving.
:rtype: str
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
# Get content
content = self._save_cxe_transform(
request, editing, values.get('content', ''))
# Debug
# tree = load_xml2('content.xml', data=content)[0]
# print('-' * 80, 'OUT')
# print(etree.tostring(
# tree, pretty_print=True, encoding='utf-8',
# xml_declaration=True).decode('utf8'))
# Validate
relaxngs = relaxng4validation(self.relaxng)
tree, error = load_xml2(
'content.xml', relaxngs=relaxngs, data=content, noline=True)
if error is not None and tree is None:
return translate(error, request=request)
# Get content
content = etree.tostring(
tree, pretty_print=True, encoding='utf-8',
xml_declaration=True).decode('utf8')
if 'regex' in editing and editing['regex'][1]:
content = apply_regex(
request, self.abspath_from_home(editing['regex'][1]), content)
tree, error = load_xml2(
'content.xml', relaxngs=relaxngs, data=content, noline=True)
if error is not None and tree is None:
return translate(error, request=request)
# Save
try:
abs_path = ciopath.absolute_path(warehouse.root)
with open(abs_path, 'w', encoding='utf8') as hdl:
hdl.write(content)
except (OSError, IOError):
return _('Unable to save "${f}"', {'f': str(ciopath)})
# Go on
if not go_on:
self.edit_finalization(
request, warehouse, ciopath,
translate(
_('Online editing in "WYSIWYM" mode'), request=request))
self._update_panel(request, warehouse, ciopath, tree)
return None
# -------------------------------------------------------------------------
def _save_cxe_transform(
self, request: Request, editing: dict, content: str) -> str:
"""Possibly convert the content before saving and return it as a
string.
:type request: pyramid.request.Request
:param request:
Current request.
:param dict editing:
Dictionary representing the editing.
:param content:
Content of the file
:rtype: str
"""
# XSL
content = re_sub(
' cxens="([a-z]+):([a-zA-Z0-9/:._-]+)"', r' xmlns:\1="\2"',
special_unprotect(content))
uid = f'{self.uid}:cxe_out'
if uid not in self._xslt:
try:
self._xslt[uid] = etree.XSLT(etree.parse(
self.abspath_from_home(editing['xsl'][1])))\
if 'xsl' in editing and editing['xsl'][1] else None
except (IOError, etree.XSLTParseError,
etree.XMLSyntaxError) as err:
self._log_error(err, request)
self._xslt[uid] = None
if self._xslt[uid] is not None:
tree, error = load_xml2('content.xml', data=content)
if error is not None:
self._log_error(error, request)
return ''
try:
tree = self._xslt[uid](tree)
except (etree.XSLTApplyError, TypeError) as err:
self._log_error(err, request)
return ''
content = str(tree)
# Regular expressions
if 'regex' in editing and editing['regex'][1]:
content = apply_regex(
request, self.abspath_from_home(editing['regex'][1]), content)
return content
# -------------------------------------------------------------------------
def _save_mask(
self, request: Request, warehouse: Warehouse, ciopath: CioPath,
values: dict, go_on: bool,
original: etree.ElementTree) -> str | None:
"""Update XML according to a mask. This method must be overridden by
the derived class.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` to the file.
:param dict values:
Values of the form.
:param bool go_on:
``True`` if the modification continues after saving.
:type original: lxml.etree._ElementTree
:param original:
Initial content of the file as a XML DOM object.
:rtype: :class:`str` or ``None``
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
# pylint: disable = unused-argument
# Validate
if self.relaxng is not None:
error = validate_xml(
original, relaxngs=relaxng4validation(self.relaxng))
if error is not None: # pragma: nocover
return translate(error, request=request)
# Save
try:
original.write(
ciopath.absolute_path(warehouse.root),
pretty_print=True,
encoding='utf-8',
xml_declaration=True)
except (OSError, IOError): # pragma: nocover
return _('Unable to save "${f}"', {'f': str(ciopath)})
# Go on
if not go_on:
self.edit_finalization(
request, warehouse, ciopath,
translate(_('Online editing in "mask" mode'), request=request))
self._update_panel(request, warehouse, ciopath, original)
return None
# -------------------------------------------------------------------------
def _save_xml(
self, request: Request, warehouse: Warehouse, ciopath: CioPath,
values: dict, go_on: bool) -> str | None:
"""Update XML.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: .lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` to the file.
:param dict values:
Values of the form.
:param bool go_on:
``True`` if the modification continues after saving.
:rtype: :class:`str` or ``None``
"""
# Validate
xml = values.get('content', '').replace('&', '#amp;')\
.replace('<', '#lt;').replace('>', '#gt;')\
.replace('&', '&').replace('#amp;', '&')\
.replace('#lt;', '<').replace('#gt;', '>')\
.replace('’', "'").replace('‧', ' ')
tree, error = load_xml2(
'content.xml', relaxngs=relaxng4validation(self.relaxng), data=xml)
if error is not None and tree is None:
return translate(error, request=request)
# Save
try:
tree.write(
ciopath.absolute_path(warehouse.root),
pretty_print=True,
encoding='utf-8',
xml_declaration=True)
except (OSError, IOError): # pragma: nocover
return _('Unable to save "${f}"', {'f': str(ciopath)})
# Go on
if not go_on:
self.edit_finalization(
request, warehouse, ciopath,
translate(_('Online editing in XML mode'), request=request))
self._update_panel(request, warehouse, ciopath, tree)
return None
# -------------------------------------------------------------------------
[docs]
def xml2html(
self,
request: Request,
warehouse: Warehouse,
ciopath: CioPath,
rendering: dict,
mode: str = 'view',
tree: etree.ElementTree | None = None,
in_panel: bool = False
) -> tuple[str | None, str | None, etree.ElementTree | None]:
"""Thanks to a XSL file, return a piece of HTML to display or edit a
file.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param cioppath:
`CioPath` of the current file.
:param dict rendering:
Dictionary defining the rendering.
:param str mode: (optional)
Display mode ('view, 'frame' or 'values').
:type tree: lxml.etree.ElementTree
:param tree: (optional)
Content of the file.
:param bool in_panel: (default=False)
``True`` if it is displayed in a panel.
:rtype: tuple
:return:
A tuple such as ``(html, class, content_as_tree)``.
See: :meth:`.managers.xml.ManagerXml.view` and
:meth:`.managers.xml.ManagerXml.edit`
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
# pylint: disable = too-many-locals
# Retrieve the XSL
uid = f"{self.uid}:{rendering.get('name')}"
if uid not in self._xslt:
self._xslt[uid] = load_xslt(
self.abspath_from_home(rendering.get('xsl')))
if not isinstance(self._xslt[uid], etree.XSLT):
self._log_error(self._xslt[uid], request)
return None, None, tree
# Load the file
parser = etree.XMLParser(
remove_blank_text=True) if mode == 'frame' else None
tree, err = load_xml2(
ciopath.absolute_path(warehouse.root), data=tree, parser=parser)
if err is not None or tree is None:
self._log_error(err, request)
return None, None, tree
# Get class
class_ = None
if self.class_xpath and mode != 'values':
class_ = tree.xpath(self.class_xpath)
class_ = class_ if class_ else None
# Prepare parameters
ciotype_route = self.ciotype.route()
parent_route = ciopath.parent().route()
media_rendering = 'viewing' if mode == 'view' else 'editing'
params = { # yapf: disable
'mode': f'"{mode}"',
'theme': f'"{theme_static_prefix(request)}"',
'manager_uid': '"{0}"'.format(
rendering.get('manager_uid', self.uid)),
'in_panel': '1' if in_panel else '0',
'language': '"{0}"'.format(
request.session.get('lang', 'en') if request else ''),
'warehouse_id': f'"{warehouse.uid}"', # deprecated
'input_ciotype': f'"{str(self.ciotype)}"',
'input_ciopath': f'"{str(ciopath)}"',
'input_warehouse': f'"{ciopath.wid}"',
'input_path': f'"{ciopath.path}"',
'input_directory': '"{0}/"'.format(ciopath.directory() or '.'),
'input_filename': '"{0}"'.format(ciopath.file_name() or ''),
'route_view': '"{0}"'.format(
request.route_path('file_view', ciotype='-', ciopath='')),
'route_edit': '"{0}"'.format(
request.route_path('file_edit', ciotype='-', ciopath='')),
'route_preview': '"{0}"'.format(
request.route_path(
'file_preview', ciotype=ciotype_route, ciopath='')),
'route_download': '"{0}"'.format(
request.route_path('file_download', ciopath='')),
'route_image': '"{0}"'.format(request.route_path(
'media_download', rendering=media_rendering,
media_type='image', ciotype=ciotype_route,
ciopath=parent_route)),
'route_audio': '"{0}"'.format(request.route_path(
'media_download', rendering=media_rendering,
media_type='audio', ciotype=ciotype_route,
ciopath=parent_route)),
'route_video': '"{0}"'.format(request.route_path(
'media_download', rendering=media_rendering,
media_type='video', ciotype=ciotype_route,
ciopath=parent_route))
}
if 'static' in rendering:
params['static'] = '"{0}"'.format(
self.abspath_from_home(rendering['static']))
if 'css' in rendering:
params['css'] = '"{0}"'.format(rendering['css'])
# XSL transform
try:
html = self._xslt[uid](tree, **params)
except (etree.XSLTApplyError, TypeError) as error:
self._log_error(error, request)
return None, class_, tree
html = etree.tostring(
etree.ElementTree(html.getroot()), encoding='utf-8').decode(
'utf8') if html.getroot() is not None else str(html)
return html, class_, tree
# -------------------------------------------------------------------------
def _update_panel(
self, request: Request, warehouse: Warehouse, ciopath: CioPath,
content: etree.ElementTree):
"""Update panel. This method must be overridden by the derived class.
:type request: pyramid.request.Request
:param request:
Current request.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:type ciopath: ciowarehouse2.lib.ciopath.CioPath
:param ciopath:
`CioPath` to the file.
:type content: lxml.etree._ElementTree
:param content:
Content of the file as a XML DOM object.
"""
if self.panel is not None:
panel = request.registry['panels'].get(self.panel.uid)
if panel is not None and panel.is_open(request):
content = panel.render_content(
request, warehouse, ciopath, content)
panel.set_value(request, 'content', Literal(content))
panel.set_value(request, 'modified', False)
# -------------------------------------------------------------------------
def _fix_renderings(self, request: Request, available: dict) -> dict:
"""Possibly Fix URLs.
:type request: pyramid.request.Request
:param request:
Current request.
:param dict available:
Available renderings.
:rtype: dictionary
"""
katex_local = 'modules' in request.registry \
and 'ciokatex' in request.registry['modules'] \
and 'ciokatex' not in request.registry['modules_off']
katex_css = KATEX_CSS_LOCAL if katex_local else KATEX_CSS_CDN
katex_js = KATEX_JS_LOCAL if katex_local else KATEX_JS_CDN
mathlive_local = 'modules' in request.registry \
and 'ciomathlive' in request.registry['modules'] \
and 'ciomathlive' not in request.registry['modules_off']
mathlive_css = MATHLIVE_CSS_LOCAL \
if mathlive_local else MATHLIVE_CSS_CDN
mathlive_js = MATHLIVE_JS_LOCAL if mathlive_local else MATHLIVE_JS_CDN
compute_engine_js = COMPUTE_ENGINE_JS_LOCAL \
if mathlive_local else COMPUTE_ENGINE_JS_CDN
for renderings in ('viewings', 'editings'):
for rendering in available.get(renderings, ''):
urls = []
for url in rendering.get('css') or '':
urls.append(
url.format(
katex_css=katex_css, mathlive_css=mathlive_css))
rendering['css'] = tuple(urls)
urls = []
for url in rendering.get('js') or '':
urls.append(
url.format(
katex_js=katex_js,
mathlive_js=mathlive_js,
compute_engine_js=compute_engine_js))
rendering['js'] = tuple(urls)
return available
# -------------------------------------------------------------------------
@classmethod
def _transform_pi(
cls,
warehouse: Warehouse,
rendering: dict,
html: str,
request: Request | None = None) -> str:
"""Fix processing instructions.
:type warehouse: ciowarehouse2.lib.warehouse.Warehouse
:param warehouse:
Object describing the warehouse containing the file.
:param dict rendering:
Dictionary defining the rendering.
:param str html:
HTML content to fix.
:type request: pyramid.request.Request
:param request: (optional)
Current request.
:rtype: str
:return:
Fixed HTML content
"""
# pylint: disable = unused-argument
return html