Source code for cioxml2.managers.xml

"""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;', '&amp;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;', '#amp;')\ .replace('&lt;', '#lt;').replace('&gt;', '#gt;')\ .replace('&', '&amp;').replace('#amp;', '&amp;')\ .replace('#lt;', '&lt;').replace('#gt;', '&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