Add language tags definition, caret and bubbles in editor.
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 23 Aug 2021 12:39:57 +0000 (14:39 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 23 Aug 2021 12:39:57 +0000 (14:39 +0200)
27 files changed:
src/redakcja/settings/__init__.py
src/redakcja/static/css/html.scss
src/redakcja/static/css/master.css
src/redakcja/static/js/wiki/caret.js [new file with mode: 0644]
src/redakcja/static/js/wiki/view_editor_wysiwyg.js
src/redakcja/static/js/wiki/view_properties.js
src/redakcja/static/js/wiki/wikiapi.js
src/redakcja/static/js/wiki/xslt.js
src/redakcja/urls.py
src/wiki/templates/wiki/document_details_base.html
src/wiki/templates/wiki/tabs/wysiwyg_editor.html
src/wlxml/__init__.py [new file with mode: 0644]
src/wlxml/admin.py [new file with mode: 0644]
src/wlxml/apps.py [new file with mode: 0644]
src/wlxml/migrations/0001_initial.py [new file with mode: 0644]
src/wlxml/migrations/0002_initial_data.py [new file with mode: 0644]
src/wlxml/migrations/0003_auto_20210813_2321.py [new file with mode: 0644]
src/wlxml/migrations/0004_auto_20210814_1052.py [new file with mode: 0644]
src/wlxml/migrations/0005_tag_example_txt.py [new file with mode: 0644]
src/wlxml/migrations/__init__.py [new file with mode: 0644]
src/wlxml/models.py [new file with mode: 0644]
src/wlxml/templates/wlxml/editor.css [new file with mode: 0644]
src/wlxml/templates/wlxml/tag_detail.html [new file with mode: 0644]
src/wlxml/templates/wlxml/tag_list.html [new file with mode: 0644]
src/wlxml/templates/wlxml/wl2html.xsl [new file with mode: 0644]
src/wlxml/urls.py [new file with mode: 0644]
src/wlxml/views.py [new file with mode: 0644]

index ba7a738..dca64e5 100644 (file)
@@ -98,6 +98,7 @@ INSTALLED_APPS = (
     'toolbar',
     'apiclient',
     'email_mangler',
+    'wlxml.apps.WlxmlConfig',
 )
 
 if DEBUG:
@@ -175,6 +176,7 @@ PIPELINE = {
                 'js/slugify.js',
 
                 # wiki scripts
+                'js/wiki/caret.js',
                 'js/wiki/wikiapi.js',
                 'wiki/js/themes.js',
                 'js/wiki/xslt.js',
index b716bc5..0b9e943 100644 (file)
     max-width: 36em;
 }
 
+
+// unused?
 .htmlview #toc {
     display: none;
 }
 
+// where is it used, if any?
 .htmlview a {
     color: blue;
     text-decoration: none;
     text-decoration: none;
 }
 
-.htmlview .nazwa_utworu {
-    font-size: 3em;
-    margin: 1.5em 0;
-    text-align: center;
-    line-height: 1.5em;
-    font-weight: bold;
-}
-
-.htmlview .autor_utworu,
-.htmlview .dzielo_nadrzedne,
-.htmlview .naglowek_czesc,
-.htmlview .srodtytul,
-.htmlview .naglowek_akt
-{
-    font-size: 2em;
-    margin: 1.5em 0 0;
-    font-weight: bold;
-    line-height: 1.5em;
-}
-
-.htmlview .podtytul,
-.htmlview .naglowek_scena,
-.htmlview .naglowek_rozdzial
-{
-    font-size: 1.5em;
-    margin: 1.5em 0 0;
-    font-weight: normal;
-    line-height: 1.5em;
-}
-
-.htmlview .naglowek_podrozdzial,
-.htmlview .naglowek_osoba
-{
-    font-size: 1em;
-    margin: 1.5em 0 0;
-    line-height: 1.5em;
-    font-weight: bold;
-}
-
+// unused?
 .htmlview p {
     margin: 0;
 }
 
-.htmlview #footnotes div {
-    margin: 1.5em 0 0 0;
-}
-
-.htmlview #footnotes p {
-    margin-left: 2.5em;
-    font-size: 0.875em;
-}
-
-.htmlview blockquote {
-    font-size: 0.875em;
-}
-
 .htmlview pre {
        overflow-x: auto; /* Use horizontal scroller if needed; for Firefox 2, not needed in Firefox 3 */
        white-space: pre-wrap; /* css-3 */
 /* =================== */
 /* = Custom elements = */
 /* =================== */
-.htmlview .autor_utwor {
-    font-size: 0.5em;
-    display: block;
-    line-height: 1.5em;
-    margin-bottom: 0.25em;
-}
-
-.htmlview .dzielo_nadrzedne {
-    /* */
-}
-
-.htmlview .podtytul {
-    /* */
-}
-
-.htmlview .didaskalia {
-    font-style: italic;
-    margin: 0.5em 0 0 1.5em;
-}
-
-.htmlview .kwestia {
-    margin: 0.5em 0 0;
-}
 
 .htmlview .strofa {
     margin: 1.5em 0 0.5em auto;
     margin-left: 6em;
 }
 
-.htmlview .strofa .wers_do_prawej {
-    text-align: right;
-}
-
 /* błędne wersy */
-.htmlview *:not(.strofa) > *[x-verse]::after {
+.htmlview *:not([x-node='strofa']) > *[x-verse]::after {
     content: "Ten wers znajduje się poza strofą.";
     display: inline;
     background: red;
     margin: 0;
 }
 
-.htmlview .akap, .htmlview .akap_cd, .htmlview .akap_dialog {
-    text-align: justify;
-    margin: 1.5em 0 0;
-}
-
-.htmlview .motto_container {
-       display: inline-block;
-       margin: 1.5em 0 0;
-       clear: right;
-}
-
-.htmlview .motto {
-    text-align: justify;
-    font-style: italic;
-}
-
-.htmlview p.motto_podpis {
-       position: relative;
-       right: -3em;
-       text-align: right;
-}
-
-.htmlview div.fragment {
-    border-bottom: 0.1em solid #999;
-    padding-bottom: 1.5em;
-}
-
-.htmlview div.nota p, .htmlview div.dedykacja p {
-    text-align: right;
-    font-style: italic;
-}
 
 .htmlview div.ramka {
     border: 1px darkgray solid;
 }
 
-.htmlview .nota_red {
-    background-color: #eee;
-    border: 1px solid #888;
-    -moz-border-radius: 10px;
-    -webkit-border-radius: 10px;
-    display: block;
-    padding: 0 1em 1em 1em;
-}
-
-.htmlview hr.sekcja_swiatlo {
-    margin: 2em 0;
-    visibility: hidden;
-}
-
-.htmlview hr.separator_linia {
-    margin: 1.5em 0;
-    border: none;
-    border-bottom: 0.1em solid #000;
-}
 
 .htmlview hr.sekcja_asterysk {
     border: none;
     text-align: center;
 }
 
-.htmlview hr.sekcja_asterysk:after {
-    content: "*";
-}
-
 .htmlview div.lista_osob ol {
     list-style: none;
     padding: 0 0 0 1.5em;
 }
 
-.htmlview p.miejsce_czas {
-    font-style: italic;
-}
-
-.htmlview .mat, .htmlview .slowo_obce, .htmlview .tytul_dziela, .htmlview .didaskalia {
-    font-style: italic;
-}
-
-
-.htmlview .indeks_dolny {
-    font-style: normal;
-    vertical-align: sub;
-    font-size: .9em;
-}
-
-.htmlview {
-    .wyroznienie {
-        //    letter-spacing: 0.1em;
-        background: #dfd;
-        border: 1px solid #0f0;
-    }
-    .slowo_obce {
-        background: #ddf;
-        border: 1px solid #88f;
-    }
-    .tytul_dziela {
-        background: #fdf;
-        border: 1px solid #f8f;
-    }
-}
-
-
-.htmlview .osoba {
-    font-style: normal;
-    font-variant: small-caps;
-}
-
-.htmlview em.wieksze_odstepy {
-    font-style: normal;
-    word-spacing: 1em;
-}
-
 .htmlview .ilustr img {
     max-width: 100%;
 }
@@ -397,7 +226,7 @@ div[x-node] > .uwaga {
     background: green;
 }
 
-.htmlview .motyw {
+.htmlview [x-node='motyw'] {
     /* position: absolute; */
     float: right;
     left: auto;
@@ -471,51 +300,36 @@ div[x-node] > .uwaga {
  * Przypisy w tekście
  */
  
-.htmlview .annotation-inline-box:hover > span[x-annotation-box]{
-    display: block;
-}
-
-.htmlview .annotation-inline-box > span[x-annotation-box] {
-    display: none;
-    width: 300px;
-    font-size: 10pt;
-    line-height: 12pt;
-    font-weight: normal;
-    font-style: normal;
-    background: #fffe93;
-    border: 1px solid black;
-    border-radius: 10px;
-    -moz-border-radius: 10px;
-    -webkit-border-radius: 10px;
-    padding: 3px 5px;
-    text-decoration: none;
-    z-index: 600;
-}
-
-/*
- * Przypisy na końcu utworu (aktualnie nieuzywane)
- */
-.htmlview .annotations-block {
-    counter-reset: secondary;
-}
-
-.htmlview .annotations-block .annotation-body {
-    position: relative;
-    padding-left: 2.5em;
-    padding-top: 0.2em;
-}
-
-.htmlview .annotations-block .annotation-backref {
-    position: absolute;
-    top: 0.4em;
-    left: -0.4em;
-    width: 2.5em;
-    text-align: right;
-}
+.htmlview .annotation-inline-box {
+    &:hover > span[x-annotation-box] {
+        display: block;
+    }
 
-.htmlview .annotations-block .annotation-backref:before {
-    content: "[" counter(secondary) "]";
-    counter-increment: secondary;
+    > span[x-annotation-box] {
+        display: none;
+        width: 300px;
+        font-size: 10pt;
+        line-height: 12pt;
+        font-weight: normal;
+        font-style: normal;
+        background: #fffe93;
+        border: 1px solid black;
+        border-radius: 10px;
+        -moz-border-radius: 10px;
+        -webkit-border-radius: 10px;
+        padding: 3px 5px;
+        text-decoration: none;
+        z-index: 600;
+
+        position: absolute;
+        left: 0;
+        top: 1em;
+
+        &.editing {
+            display: block;
+        }
+    }
+    
 }
 
 /*
index 566183e..9d3744b 100644 (file)
@@ -161,6 +161,17 @@ img.tabclose {
 #simple-editor {
     display: flex;
     flex-direction: column;
+    position: relative;
+}
+
+#simple-editor #bubbles {
+    position: absolute;
+    bottom: 10px;
+    right: 20px;
+}
+#simple-editor #bubbles .badge {
+    display: block;
+    margin-top: 5px;
 }
 
 .htmlview {
@@ -178,14 +189,9 @@ img.tabclose {
        opacity: 0.4;
 }
 
-.poezja_cyt {
-    margin:1.5em 2em 0;
-    font-size:0.875em
-}
 
-.wers_akap {
-       padding-left: 1em;
-}
+
+
 
 .saveNotify {
     position:absolute; 
diff --git a/src/redakcja/static/js/wiki/caret.js b/src/redakcja/static/js/wiki/caret.js
new file mode 100644 (file)
index 0000000..8b3cbd8
--- /dev/null
@@ -0,0 +1,256 @@
+class Caret {
+    constructor(view) {
+        let self = this;
+        self.view = view;
+        self.singleClick = false;
+        
+        let caret = this.element = $('<span id="caret"><textarea></textarea></span>');
+        
+        // When user writes into caret, add it to the document.
+        $('textarea', caret).on('input', function() {
+            let v = $(this).val();
+            $(this).val('');
+            self.insertChar(v);
+            
+        });
+        
+        // On click on x-node element, set caret position.
+        self.view.on('click', '*[x-node]', function(e) {
+            if (e.redakcja_caret_inserted) return;
+            e.redakcja_caret_inserted = true;
+            
+            if (self.singleClick) {
+                self.singleClick = false;
+                return;
+            }
+            
+            self.detach();
+            
+            var selection = window.getSelection();
+            if (!selection.isCollapsed) return;
+            
+            self.singleClick = true;
+            setTimeout(function() {
+                if (self.singleClick) {
+                    self.element.insertBefore(
+                        selection.anchorNode.splitText(
+                            selection.anchorOffset
+                        )
+                    )
+                    self.focus();
+                }
+                self.singleClick = false;
+            }, 250);
+            
+        });
+        
+        self.element.on('keydown', function(e) {
+            console.log('KEY');
+            
+            // TODO:
+            // delete selection?
+            
+            // cases:
+            // we are in <akap> (no going up)
+            // we are in <wyroznienie> (can go up)
+            // we are next to <wyroznienie> (can go inside)
+            
+            switch (e.key) {
+            case "ArrowRight":
+                if (e.shiftKey) {
+                    self.detach();
+                    return;
+                }
+                
+                self.moveRight();
+                break;
+            case "ArrowLeft":
+                if (e.shiftKey) {
+                    self.detach();
+                    return;
+                }
+                
+                self.moveLeft();
+                break;
+            case "ArrowUp":
+                if (e.shiftKey) {
+                    self.detach();
+                    return;
+                }
+                break;
+            case "ArrowDown":
+                if (e.shiftKey) {
+                    self.detach();
+                    return;
+                }
+                break;
+            case "Backspace":
+                self.deleteBefore();
+                break;
+            case "Delete":
+                self.deleteAfter();
+                break;
+            case "Enter":
+                self.splitBlock();
+                break;
+                //                default:
+                //                    console.log('key', e.key, e.code);
+            }
+        })
+    }
+    
+    get attached() {
+        return this.element.parent().length;
+    }
+    
+    detach() {
+        console.log(this.view);
+        
+        let p;
+        if (this.attached) {
+            p = this.element.parent()[0]
+            this.element.detach();
+            p.normalize()
+        }
+    }
+    
+    focus() {
+        $("textarea", self.element).focus();
+    }
+    
+    normalize() {
+        this.element.parent()[0].normalize();
+    }
+    
+    insertChar(ch) {
+        $(document.createTextNode(ch)).insertBefore(this.element);
+        this.normalize();
+    }
+    
+    deleteBefore() {
+        let contents = this.element.parent().contents();
+        // Find the text before caret.
+        let textBefore = contents[contents.index(this.element) - 1];
+        
+        // Should be text, but what if not?
+        textBefore.textContent = textBefore.textContent.substr(0, textBefore.textContent.length - 1);
+        this.normalize();
+        
+    }
+    
+    deleteAfter() {
+        let contents = this.element.parent().contents();
+        // Find the text after caret.
+        let textAfter = contents[contents.index(this.element) + 1];
+        textAfter.textContent = textAfter.textContent.substr(1);
+    }
+    
+    splitBlock() {
+        let splitter = this.element;
+        let parent, newParent, splitIndex, index;
+        
+        while (!splitter.is('div[x-node]')) {
+            parent = splitter.parent();
+            splitIndex = parent.contents().index(splitter);
+            newParent = parent.clone();
+            index = parent.contents().length - 1;
+            while (index >= splitIndex) {
+                newParent.contents()[index].remove();
+                --index;
+            }
+            while (index >= 0) {
+                console.log(newParent, index);
+                parent.contents()[index].remove();
+                -- index;
+            }
+            newParent.insertBefore(parent);
+            
+            console.log('split', parent);
+            splitter = parent;
+        }
+    }
+    
+    moveLeft() {
+        this.move({
+            move: -1,
+            edge: (i, l) => {return !i;},
+            enter: (l) => {return l - 1;},
+            splitTarget: (t) => {return t.splitText(t.length - 1);},
+            noSplitTarget: (t) => {return t.splitText(t.length);},
+        })
+    }
+    
+    moveRight() {
+        this.move({
+            move: 1,
+            edge: (i, l) => {return i == l - 1;},
+            enter: (l) => {return 0;},
+            splitTarget: (t) => {return t.splitText(1);},
+            noSplitTarget: (t) => {return t;},
+        })
+    }
+    
+    move(opts) {
+        if (!this.attached) return;
+        
+        this.normalize();
+        
+        let contents = this.element.parent().contents();
+        let index = contents.index(this.element);
+        let target, moved, oldparent;
+        
+        let parent = this.element.parent()[0];
+        
+        if (opts.edge(index, contents.length)) {
+            // We're at the end -- what to do?
+            // can we go up?
+            
+            if (parent.nodeName == 'EM') {
+                oldparent = parent;
+                parent = parent.parentNode;
+                contents = $(parent).contents();
+                index = contents.index(oldparent);
+            }
+        }
+        
+        index += opts.move;
+        target = contents[index];
+        moved = false;
+        
+        while (target.nodeType == 1) {
+            // we've encountered a node.
+            // can we go inside?
+            
+            if (target.nodeName == 'EM') {
+                // enter
+                parent = $(target);
+                contents = parent.contents();
+                index = opts.enter(contents.length);
+                target = contents[index];
+                
+                // what if it has no elements?
+            } else {
+                // skip
+                index += opts.move; // again, what if end?
+                target = contents[index];
+                moved = true;
+            }
+            
+            // if editable?
+            // what if editable but empty?
+            
+        }
+        
+        if (target.nodeType == 3) {
+            if (!moved) {
+                target = opts.splitTarget(target);
+            } else {
+                target = opts.noSplitTarget(target);
+            }
+            
+            this.element.insertBefore(target);
+        }
+        this.normalize();
+        this.focus();
+    }
+}
index 82920fd..4554bbe 100644 (file)
@@ -1,229 +1,4 @@
 (function($) {
-    class Caret {
-        constructor(view) {
-            self = this;
-            self.view = view;
-            self.singleClick = false;
-            
-            let caret = this.element = $('<span id="caret"><textarea></textarea></span>');
-
-            // When user writes into caret, add it to the document.
-            $('textarea', caret).on('input', function() {
-                let v = $(this).val();
-                $(this).val('');
-                self.insertChar(v);
-
-            });
-
-            // On click on x-node element, set caret position.
-            self.view.on('click', '*[x-node]', function(e) {
-                if (e.redakcja_caret_inserted) return;
-                e.redakcja_caret_inserted = true;
-
-                if (self.singleClick) {
-                    self.singleClick = false;
-                    return;
-                }
-                
-                self.detach();
-
-                var selection = window.getSelection();
-                if (!selection.isCollapsed) return;
-
-                self.singleClick = true;
-                setTimeout(function() {
-                    if (self.singleClick) {
-                        self.element.insertBefore(
-                            selection.anchorNode.splitText(
-                                selection.anchorOffset
-                            )
-                        )
-                        self.focus();
-                    }
-                    self.singleClick = false;
-                }, 250);
-
-            });
-
-            self.view.on('keydown', function(e) {
-                // TODO:
-                // Enter (split block)
-                // delete selection?
-
-                // cases:
-                // we are in <akap> (no going up)
-                // we are in <wyroznienie> (can go up)
-                // we are next to <wyroznienie> (can go inside)
-
-                switch (e.key) {
-                case "ArrowRight":
-                    if (e.shiftKey) {
-                        self.detach();
-                        return;
-                    }
-
-                    self.moveRight();
-                    break;
-                case "ArrowLeft":
-                    if (e.shiftKey) {
-                        self.detach();
-                        return;
-                    }
-
-                    self.moveLeft();
-                    break;
-                case "ArrowUp":
-                    if (e.shiftKey) {
-                        self.detach();
-                        return;
-                    }
-                    break;
-                case "ArrowDown":
-                    if (e.shiftKey) {
-                        self.detach();
-                        return;
-                    }
-                    break;
-                case "Backspace":
-                    self.deleteBefore();
-                    break;
-                case "Delete":
-                    self.deleteAfter();
-                    break;
-//                default:
-//                    console.log('key', e.key, e.code);
-                }
-            })
-        }
-
-        get attached() {
-            return this.element.parent().length;
-        }
-
-        detach() {
-            let p;
-            if (this.attached) {
-                p = this.element.parent()[0]
-                this.element.detach();
-                p.normalize()
-            }
-        }
-
-        focus() {
-            $("textarea", self.element).focus();
-        }
-
-        normalize() {
-            this.element.parent()[0].normalize();
-        }
-
-        insertChar(ch) {
-            $(document.createTextNode(ch)).insertBefore(this.element);
-            this.normalize();
-        }
-
-        deleteBefore() {
-            let contents = this.element.parent().contents();
-            // Find the text before caret.
-            let textBefore = contents[contents.index(this.element) - 1];
-
-            // Should be text, but what if not?
-            textBefore.textContent = textBefore.textContent.substr(0, textBefore.textContent.length - 1);
-            this.normalize();
-
-        }
-
-        deleteAfter() {
-            let contents = this.element.parent().contents();
-            // Find the text after caret.
-            let textAfter = contents[contents.index(this.element) + 1];
-            textAfter.textContent = textAfter.textContent.substr(1);
-        }
-
-        moveLeft() {
-            this.move({
-                move: -1,
-                edge: (i, l) => {return !i;},
-                enter: (l) => {return l - 1;},
-                splitTarget: (t) => {return t.splitText(t.length - 1);},
-                noSplitTarget: (t) => {return t.splitText(t.length);},
-            })
-        }
-
-        moveRight() {
-            this.move({
-                move: 1,
-                edge: (i, l) => {return i == l - 1;},
-                enter: (l) => {return 0;},
-                splitTarget: (t) => {return t.splitText(1);},
-                noSplitTarget: (t) => {return t;},
-            })
-        }
-
-        move(opts) {
-            if (!this.attached) return;
-            this.normalize();
-
-            let contents = this.element.parent().contents();
-            let index = contents.index(this.element);
-            let target, moved, oldparent;
-
-            let parent = this.element.parent()[0];
-
-            if (opts.edge(index, contents.length)) {
-                // We're at the end -- what to do?
-                // can we go up?
-
-                if (parent.nodeName == 'EM') {
-                    oldparent = parent;
-                    parent = parent.parentNode;
-                    contents = $(parent).contents();
-                    index = contents.index(oldparent);
-                }
-            }
-
-            index += opts.move;
-            target = contents[index];
-            moved = false;
-
-            while (target.nodeType == 1) {
-                // we've encountered a node.
-                // can we go inside?
-
-                if (target.nodeName == 'EM') {
-                    // enter
-                    parent = $(target);
-                    contents = parent.contents();
-                    index = opts.enter(contents.length);
-                    target = contents[index];
-
-                    // what if it has no elements?
-                } else {
-                    // skip
-                    index += opts.move; // again, what if end?
-                    target = contents[index];
-                    moved = true;
-                }
-
-                // if editable?
-                // what if editable but empty?
-
-            }
-
-            if (target.nodeType == 3) {
-                if (!moved) {
-                    target = opts.splitTarget(target);
-                } else {
-                    target = opts.noSplitTarget(target);
-                }
-
-                this.element.insertBefore(target);
-            }
-            this.normalize();
-            this.focus();
-        }
-    }
-
 
     /* Show theme to the user */
     function selectTheme(themeId){
                     }
                     if (editable.is('.annotation-inline-box')) {
                         $('*[x-annotation-box]', editable).css({
-                            position: 'absolute',
-                            left: event.clientX - editable.offset().left + 5,
-                            top: event.clientY - editable.offset().top + 5
+//                            left: event.clientX - editable.offset().left + 5,
+//                            top: event.clientY - editable.offset().top + 5
                         }).show();
                     }
                     else {
-                        $('*[x-annotation-box]').hide();
+//                        $('*[x-annotation-box]').hide();
                     }
                 });
 
+                perspective.caret = new Caret(element);
+                
                 $('#insert-reference-button').click(function(){
                     addReference();
                     return false;
                 selectTheme($(this).attr('theme-class'));
             });
 
+            element.on('click', '.annotation', function(event) {
+                event.preventDefault();
+                $('[x-annotation-box]', $(this).parent()).toggleClass('editing');
+                
+            });
+
             old_callback.call(this);
         };
 
 
     VisualPerspective.prototype = new $.wiki.Perspective();
 
-    VisualPerspective.prototype.freezeState = function(){
-
-    };
-
     VisualPerspective.prototype.onEnter = function(success, failure){
         $.wiki.Perspective.prototype.onEnter.call(this);
 
                 var htmlView = $('#html-view');
                 htmlView.html(element);
 
-                perspective.caret = new Caret(htmlView);
-
-
-
                 htmlView.find('*[x-node]').dblclick(function(e) {
                     if($(e.target).is('textarea'))
                         return;
     VisualPerspective.prototype.onExit = function(success, failure){
         var self = this;
 
+        self.caret.detach();
+        
         $.blockUI({
             message: 'Zapisywanie widoku...'
         });
         });
     };
 
-
-    
     $.wiki.VisualPerspective = VisualPerspective;
 
 })(jQuery);
index e6ad1ca..5af43e6 100644 (file)
                 self.edit($(this).data('node'));
             });
 
+            $(document).on('click', '#bubbles .badge', function(e) {
+                self.edit($(this).data('node'));
+            });
+
             self.$pane.on('change', '.form-control', function() {
                 let $input = $(this);
 
                 });
                 self.$edited;
             });
+
+            self.$pane.on('click', '.current-convert', function() {
+                self.convert($(this).attr('data-to'));
+            });
+            self.$pane.on('click', '#current-delete', function() {
+                self.delete();
+            });
+            
             
             oldCallback.call(this);
         };
 
         let $node = $(element);
         $("#parents", self.$pane).empty();
+        $("#bubbles").empty();
+
+        let b = $("<div class='badge badge-primary'></div>").text($node.attr('x-node'));
+        b.data('node', element);
+        $("#bubbles").append(b);
+
         $node.parents('[x-node]').each(function() {
             let a = $("<li class='breadcrumb-item'>").text($(this).attr('x-node'));
             a.data('node', this);
             $("#parents", self.$pane).prepend(a)
+
+            let b = $("<div class='badge badge-info'></div>").text($(this).attr('x-node'));
+            b.data('node', this);
+            $("#bubbles").append(b);
         })
+
         // It's a tag.
         node = $(element).attr('x-node');
         $("h1", self.$pane).text(node);
         // Only utwor can has matadata now.
         if (node == 'utwor') {
             // Let's find all the metadata.
-            $("> .RDF > .Description > [x-node]", $node).each(function() {
+            $("> [x-node='RDF'] > [x-node='Description'] > [x-node]", $node).each(function() {
                 $meta = $(this);
                 self.addEditField(
                     {"name": $meta.attr('x-node'),},
                 );
             });
         }
+
+
+
+        // check node type, find relevant tags
+        if ($node[0].nodeName == 'DIV') {
+            $("#current-convert").attr("data-current-type", "div");
+        } else if ($node[0].nodeName == 'EM') {
+            $("#current-convert").attr("data-current-type", "span");
+        }
     };
-        
+
+
+
     PropertiesPerspective.prototype.addEditField = function(defn, value, elem) {
         let self = this;
         let $form = $("#properties-form", self.$pane);
 
         $fg.appendTo($form);
     }
-    
+
+    PropertiesPerspective.prototype.convert = function(newtag) {
+        this.$edited.attr('x-node', newtag);
+        // TODO: take care of attributes?
+    }
+
+    PropertiesPerspective.prototype.delete = function(newtag) {
+        p = this.$edited.parent();
+        this.$edited.remove();
+        this.edit(p);
+    }
+
     $.wiki.PropertiesPerspective = PropertiesPerspective;
 
 })(jQuery);
index 2f1bceb..4e3cd2c 100644 (file)
                });
        };
 
-       /*
-        * Set document's text
-        */
-       WikiDocument.prototype.setText = function(text) {
-               return this.setDocumentProperty('text', text);
-       };
-
-       /*
-        * Set document's gallery link
-        */
-       WikiDocument.prototype.setGalleryLink = function(gallery) {
-               return this.setDocumentProperty('galleryLink', gallery);
-       };
+    /*
+     * Set document's text
+     */
+    WikiDocument.prototype.setText = function(text, setter) {
+        if (text == this.text) return;
+        
+        this.text = text;
+        this.has_local_changes = true;
 
-       /*
-        * Set document's property
-        */
-       WikiDocument.prototype.setDocumentProperty = function(property, value) {
-               if(this[property] !== value) {
-                       this[property] = value;
-                       this.has_local_changes = true;
-               }
-       };
+    };
 
+    
        /*
         * Save text back to the server
         */
index c1575fd..1980446 100644 (file)
@@ -17,7 +17,7 @@ function withStylesheets(code_block, onError)
     if (!xml2htmlStylesheet) {
        $.blockUI({message: 'Ładowanie arkuszy stylów...'});
        $.ajax({
-            url: STATIC_URL + 'xsl/wl2html_client.xsl?210709',
+            url: '/wlxml/wl2html.xsl',
             dataType: 'xml',
             timeout: 10000,
             success: function(data) {
index fe2193e..8050ae1 100644 (file)
@@ -26,6 +26,7 @@ urlpatterns = [
     url(r'^editor/', include('wiki.urls')),
     url(r'^images/', include('wiki_img.urls')),
     url(r'^cover/', include('cover.urls')),
+    url(r'^wlxml/', include('wlxml.urls')),
 ]
 
 
index 75d2911..db78c7e 100644 (file)
@@ -6,7 +6,8 @@
 {% block extrahead %}
   {% load pipeline %}
   {% stylesheet 'detail' %}
-  <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+    <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+    <link rel="stylesheet" href="/wlxml/wl.css"
 {% endblock %}
 
 {% block extrabody %}
index c94e5b1..53e14b8 100644 (file)
@@ -30,6 +30,7 @@
       {% endif %}
     </div>
   </div>
+  <div id="bubbles"></div>
   <div id="html-view" class="htmlview">
   </div>
 </div>
diff --git a/src/wlxml/__init__.py b/src/wlxml/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/wlxml/admin.py b/src/wlxml/admin.py
new file mode 100644 (file)
index 0000000..9b73b21
--- /dev/null
@@ -0,0 +1,41 @@
+from django.contrib import admin
+from . import models
+
+
+class AttributeInline(admin.TabularInline):
+    model = models.Attribute
+
+
+@admin.register(models.Tag)
+class TagAdmin(admin.ModelAdmin):
+    inlines = [AttributeInline]
+    list_display = ['name', 'type']
+    list_filter = ['type']
+    fieldsets = [
+        (None, {
+            'fields': [
+                'name',
+                'type',
+                'similar_to',
+                'description',
+                'example',
+            ]
+        }),
+        ('Editor style', {
+            'fields': [
+                'editor_css', 'editor_css_after',
+            ]
+        }),
+    ]
+
+
+
+@admin.register(models.TagUsage)
+class TagUsageAdmin(admin.ModelAdmin):
+    list_filter = ['tag']
+
+
+@admin.register(models.AttributeUsage)
+class AttributeUsageAdmin(admin.ModelAdmin):
+    list_filter = ['attribute__tag', 'attribute']
+    list_display = ['tag_usage', 'attribute', 'value']
diff --git a/src/wlxml/apps.py b/src/wlxml/apps.py
new file mode 100644 (file)
index 0000000..a565c22
--- /dev/null
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class WlxmlConfig(AppConfig):
+    name = 'wlxml'
+    verbose_name = 'WL-XML'
diff --git a/src/wlxml/migrations/0001_initial.py b/src/wlxml/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..096e532
--- /dev/null
@@ -0,0 +1,76 @@
+# Generated by Django 3.1.13 on 2021-08-13 15:44
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('documents', '0006_auto_20210706_0130'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Attribute',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=255)),
+            ],
+            options={
+                'verbose_name': 'attribute',
+                'verbose_name_plural': 'attribute',
+            },
+        ),
+        migrations.CreateModel(
+            name='Tag',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(db_index=True, max_length=255, unique=True)),
+                ('type', models.CharField(blank=True, choices=[('section', 'Section, contains blocks'), ('div', 'Block element, like a paragraph'), ('span', 'Inline element, like an emphasis'), ('sep', 'Separator, has no content'), ('aside', 'Aside content, like a footnote'), ('verse', 'Verse element')], max_length=255)),
+                ('editor_css', models.TextField(blank=True)),
+                ('editor_css_after', models.TextField(blank=True)),
+                ('similar_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='wlxml.tag')),
+            ],
+            options={
+                'verbose_name': 'tag',
+                'verbose_name_plural': 'tags',
+            },
+        ),
+        migrations.CreateModel(
+            name='TagUsage',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('chunk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.chunk')),
+                ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wlxml.tag')),
+            ],
+            options={
+                'verbose_name': 'tag usage',
+                'verbose_name_plural': 'tags usage',
+            },
+        ),
+        migrations.CreateModel(
+            name='AttributeUsage',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('value', models.CharField(blank=True, max_length=2048)),
+                ('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wlxml.attribute')),
+                ('tag_usage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wlxml.tagusage')),
+            ],
+            options={
+                'verbose_name': 'attribute usage',
+                'verbose_name_plural': 'attributes usage',
+            },
+        ),
+        migrations.AddField(
+            model_name='attribute',
+            name='tag',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wlxml.tag'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='attribute',
+            unique_together={('tag', 'name')},
+        ),
+    ]
diff --git a/src/wlxml/migrations/0002_initial_data.py b/src/wlxml/migrations/0002_initial_data.py
new file mode 100644 (file)
index 0000000..2e386c5
--- /dev/null
@@ -0,0 +1,281 @@
+# Generated by Django 3.1.13 on 2021-08-13 15:44
+
+from django.db import migrations
+
+
+initial_tags = {
+    "section": {
+        "opowiadanie": {},
+        "powiesc": {},
+        "liryka_l": {},
+        "liryka_lp": {},
+        "dramat_wierszowany_l": {},
+        "dramat_wierszowany_lp": {},
+        "dramat_wspolczesny": {},
+        "wywiad": {},
+        "nota": {},
+        "nota_red": {
+            "editor_css": """
+            background-color: #eee;
+            border: 1px solid #888;
+            border-radius: 10px;
+            display: block;
+            padding: 0 1em 1em 1em;
+            """
+        },
+        "dedykacja": {},
+        "ramka": {},
+        "lista_osob": {},
+        "dlugi_cytat": {},
+        "poezja_cyt": {
+            "editor_css": """margin: 1.5em 2em 0;
+font-size: 0.875em;"""
+        },
+        "kwestia": {
+            "editor_css": """
+                margin: 5em 0 0;
+            """
+        },
+        "wywiad_pyt": {},
+        "wywiad_odp": {},
+        "motto": {
+            "editor_css": """
+                text-align: justify;
+                font-style: italic;
+            """
+        },
+    },
+    "div": {
+        "didaskalia": {
+            "editor_css": """font-style: italic;
+margin: 0.5em 0 0 1.5em;"""
+        },
+        "naglowek_podrozdzial": {
+            "editor_css": """
+                font-size: 1em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        },
+        "naglowek_osoba": {
+            "editor_css": """
+                font-size: 1em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        }, 
+        "podtytul": {
+            "editor_css": """
+                font-size: 1.5em;
+                margin: 1.5em 0 0;
+                font-weight: normal;
+                line-height: 1.5em;
+            """
+        }, 
+        "naglowek_scena": {
+            "editor_css": """
+                font-size: 1.5em;
+                margin: 1.5em 0 0;
+                font-weight: normal;
+                line-height: 1.5em;
+            """
+        }, 
+        "naglowek_rozdzial": {
+            "editor_css": """
+                font-size: 1.5em;
+                margin: 1.5em 0 0;
+                font-weight: normal;
+                line-height: 1.5em;
+            """
+        }, 
+        "autor_utworu": {
+            "editor_css": """
+                font-size: 2em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        }, 
+        "dzielo_nadrzedne": {
+            "editor_css": """
+                font-size: 2em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        }, 
+        "naglowek_czesc": {
+            "editor_css": """
+                font-size: 2em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        }, 
+        "srodtytul": {
+            "editor_css": """
+                font-size: 2em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        }, 
+        "naglowek_akt": {
+            "editor_css": """
+                font-size: 2em;
+                margin: 1.5em 0 0;
+                font-weight: bold;
+                line-height: 1.5em;
+            """
+        }, 
+        "nazwa_utworu": {
+            "editor_css": """
+                font-size: 3em;
+                margin: 1.5em 0;
+                text-align: center;
+                line-height: 1.5em;
+                font-weight: bold;
+            """,
+        }, 
+        "naglowek_listy": {}, 
+        "lista_osoba": {}, 
+        "miejsce_czas": {
+            "editor_css": """font-style: italic;""",
+        }, 
+        "akap": {
+            "editor_css": """
+                text-align: justify;
+                margin: 1.5em 0 0;
+            """
+        }, 
+        "akap_cd": {
+            "editor_css": """
+                text-align: justify;
+                margin: 1.5em 0 0;
+            """
+        }, 
+        "akap_dialog": {
+            "editor_css": """
+                text-align: justify;
+                margin: 1.5em 0 0;
+            """
+        }, 
+        "motto_podpis": {
+            "editor_css": """
+                position: relative;
+                right: -3em;
+                text-align: right;
+            """
+        }, 
+        "uwaga": {}, 
+        "extra": {},
+    },
+    "verse": {
+        "wers_cd": {}, 
+        "wers_akap": {
+            "editor_css": """padding-left: 1em;"""
+        }, 
+        "wers_wciety": {}, 
+        "wers_do_prawej": {
+            "editor_css": """
+                text-align: right;
+            """
+        }, 
+        "wers": {},
+    },
+    "span": {
+        "tytul_dziela": {
+            "editor_css": """font-style: italic;"""
+        }, 
+        "wyroznienie": {
+            "editor_css": """font-style: italic;
+letter-spacing: 0.1em;"""
+        }, 
+        "slowo_obce": {
+            "editor_css": """font-style: italic;"""
+        }, 
+        "mat": {
+            "editor_css": """font-style: italic;"""
+        }, 
+        "didask_tekst": {}, 
+        "osoba": {
+            "editor_css": """font-style: normal;
+font-variant: small-caps;"""
+        }, 
+        "wyp_osoba": {}, 
+        "www": {}, 
+        "wieksze_odstepy": {
+            "editor_css": """font-style: normal;
+word-spacing: 1em;"""
+        }, 
+        "indeks_dolny": {
+            "editor_css": """font-style: normal;
+vertical-align: sub;
+font-size: .9em;"""
+        }, 
+        "zastepnik_wersu": {},
+    },
+    "sep": {
+        "sekcja_swiatlo": {
+            "editor_css": """
+            margin: 2em 0;
+            visibility: hidden;
+            """
+        }, 
+        "sekcja_asterysk": {
+            "editor_css": """
+            border: none;
+            text-align: center;
+            """,
+            "editor_css_after": """
+            content: "*";
+            """
+        }, 
+        "separator_linia": {
+            "editor_css": """
+            margin: 1.5em 0;
+            border: none;
+            border-bottom: 0.1em solid #000;
+            """
+        },
+    },
+    "aside": {
+        "pr": {}, 
+        "pa": {}, 
+        "pe": {}, 
+        "pt": {},
+    }
+
+    # To nie są wszystkie tagi.
+    # Brakuje:
+    # strofa?
+    # motyw,begin,end?
+    # ref?
+}
+
+
+def create_tags(apps, schema_editor):
+    Tag = apps.get_model('wlxml', 'Tag')
+    for tag_type, tags in initial_tags.items():
+        for name, props in tags.items():
+            Tag.objects.create(
+                type=tag_type,
+                name=name,
+                **props
+            )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wlxml', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            create_tags,
+            migrations.RunPython.noop
+        )
+    ]
diff --git a/src/wlxml/migrations/0003_auto_20210813_2321.py b/src/wlxml/migrations/0003_auto_20210813_2321.py
new file mode 100644 (file)
index 0000000..7ccfb3e
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.13 on 2021-08-13 23:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wlxml', '0002_initial_data'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='tag',
+            name='description',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='example',
+            field=models.TextField(blank=True),
+        ),
+    ]
diff --git a/src/wlxml/migrations/0004_auto_20210814_1052.py b/src/wlxml/migrations/0004_auto_20210814_1052.py
new file mode 100644 (file)
index 0000000..287c9ea
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.13 on 2021-08-14 10:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wlxml', '0003_auto_20210813_2321'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='tag',
+            name='example_html',
+            field=models.FileField(blank=True, upload_to='wlxml/tag/example/html/'),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='example_pdf',
+            field=models.FileField(blank=True, upload_to='wlxml/tag/example/pdf/'),
+        ),
+    ]
diff --git a/src/wlxml/migrations/0005_tag_example_txt.py b/src/wlxml/migrations/0005_tag_example_txt.py
new file mode 100644 (file)
index 0000000..ca9eea9
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.13 on 2021-08-14 11:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wlxml', '0004_auto_20210814_1052'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='tag',
+            name='example_txt',
+            field=models.FileField(blank=True, upload_to='wlxml/tag/example/txt/'),
+        ),
+    ]
diff --git a/src/wlxml/migrations/__init__.py b/src/wlxml/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/wlxml/models.py b/src/wlxml/models.py
new file mode 100644 (file)
index 0000000..0b910a8
--- /dev/null
@@ -0,0 +1,164 @@
+from io import BytesIO
+from django.apps import apps
+from django.core.files import File
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+from librarian import DocProvider
+from librarian.parser import WLDocument as LegacyWLDocument
+from librarian.builders import StandaloneHtmlBuilder, TxtBuilder
+from librarian.document import WLDocument
+
+
+class Tag(models.Model):
+    name = models.CharField(max_length=255, unique=True, db_index=True)
+    type = models.CharField(max_length=255, choices=[
+        ('section', _('Section, contains blocks')),
+        ('div', _('Block element, like a paragraph')),
+        ('span', _('Inline element, like an emphasis')),
+        ('sep', _('Separator, has no content')),
+        ('aside', _('Aside content, like a footnote')),
+        ('verse', _('Verse element')),
+    ], blank=True)
+    similar_to = models.ForeignKey('self', models.PROTECT, null=True, blank=True)
+    description = models.TextField(blank=True)
+    example = models.TextField(blank=True)
+
+    example_html = models.FileField(upload_to='wlxml/tag/example/html/', blank=True)
+    example_pdf = models.FileField(upload_to='wlxml/tag/example/pdf/', blank=True)
+    example_txt = models.FileField(upload_to='wlxml/tag/example/txt/', blank=True)
+
+    # border_radius?
+    editor_css = models.TextField(blank=True)
+    editor_css_after = models.TextField(blank=True)
+
+    class Meta:
+        verbose_name = _('tag')
+        verbose_name_plural = _('tags')
+
+    def __str__(self):
+        return self.name
+
+    def get_absolute_url(self):
+        return reverse('wlxml_tag', args=[self.name])
+    ### allowed tags?
+
+    def save(self, **kwargs):
+        docbytes = b'''<utwor>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://redakcja.wolnelektury.pl/documents/book/brudnopis/">
+
+<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">test</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">test</dc:title>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">test</dc:date>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">test</dc:publisher>
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">test</dc:identifier.url>
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">test</dc:rights>
+
+</rdf:Description>
+</rdf:RDF>
+
+<opowiadanie>''' + self.example.encode('utf-8') + b'</opowiadanie></utwor>'
+
+
+        doc = WLDocument(filename=BytesIO(docbytes))
+
+        self.example_html.save(
+            self.name + '.html',
+            File(
+                StandaloneHtmlBuilder().build(doc).get_file()),
+            save=False)
+        self.example_txt.save(
+            self.name + '.txt',
+            File(
+                TxtBuilder().build(doc).get_file()),
+            save=False)
+
+        provider=DocProvider()
+        legacy_doc = LegacyWLDocument.from_bytes(docbytes, provider=provider)
+
+        self.example_pdf.save(
+            self.name + '.pdf',
+            File(legacy_doc.as_pdf().get_file()),
+            save=False)
+        
+
+        super().save(**kwargs)
+
+    
+
+class Attribute(models.Model):
+    tag = models.ForeignKey(Tag, models.CASCADE)
+    name = models.CharField(max_length=255)
+
+    class Meta:
+        verbose_name = _('attribute')
+        verbose_name_plural = _('attribute')
+    
+        unique_together = [
+            ('tag', 'name'),
+        ]
+
+    def __str__(self):
+        return self.name
+
+    
+class TagUsage(models.Model):
+    tag = models.ForeignKey(Tag, models.CASCADE)
+    chunk = models.ForeignKey('documents.Chunk', models.CASCADE)
+
+    class Meta:
+        verbose_name = _('tag usage')
+        verbose_name_plural = _('tags usage')
+    
+    def __str__(self):
+        return f'{self.tag.name} @ {self.chunk.slug}'
+        
+    
+    @classmethod
+    def update_chunk(cls, chunk):
+        tag_names = set()
+        attribute_items = {}
+        doc = WLDocument.from_bytes(chunk.materialize().encode('utf-8'))
+        for element in doc.edoc.iter():
+            tag_names.add(element.tag)
+            for k, v in element.attrib.iteritems():
+                attribute_items.setdefault(element.tag, set()).add((k, v))
+
+        cls.objects.filter(chunk=chunk).exclude(tag__name__in=tag_names).delete()
+        for tag_name in tag_names:
+            tag, create = Tag.objects.get_or_create(name=tag_name)
+            tu, created = cls.objects.get_or_create(tag=tag, chunk=chunk)
+
+            new_attributes = attribute_items.get(tag_name, [])
+            
+            for attr in tu.attributeusage_set.all():
+                key = (attr.attribute.name, value)
+                if key not in new_attributes:
+                    attr.delete()
+                else:
+                    new_attributes.delete(key)
+
+            for k, v in new_attributes:
+                attribute, created = tag.attribute_set.get_or_create(name=k)
+                tu.attributeusage_set.create(attribute=attribute, value=v)
+
+
+    @classmethod
+    def update_all_chunks(cls):
+        Chunk = apps.get_model('documents', 'Chunk')
+        for chunk in Chunk.objects.all():
+            cls.update_chunk(chunk)
+
+
+class AttributeUsage(models.Model):
+    tag_usage = models.ForeignKey(TagUsage, models.CASCADE)
+    attribute = models.ForeignKey(Attribute, models.CASCADE)
+    value = models.CharField(max_length=2048, blank=True)
+
+    class Meta:
+        verbose_name = _('attribute usage')
+        verbose_name_plural = _('attributes usage')
+    
diff --git a/src/wlxml/templates/wlxml/editor.css b/src/wlxml/templates/wlxml/editor.css
new file mode 100644 (file)
index 0000000..58152ce
--- /dev/null
@@ -0,0 +1,10 @@
+{% for tag in object_list %}
+.htmlview [x-node="{{ tag.name }}"] {
+    {{ tag.editor_css|safe }}
+}
+{% if tag.editor_css_after %}
+.htmlview [x-node="{{ tag.name }}"]::after {
+    {{ tag.editor_css_after|safe }}
+}
+{% endif %}
+{% endfor %}
diff --git a/src/wlxml/templates/wlxml/tag_detail.html b/src/wlxml/templates/wlxml/tag_detail.html
new file mode 100644 (file)
index 0000000..f23f416
--- /dev/null
@@ -0,0 +1,23 @@
+{% extends "documents/base.html" %}
+
+
+{% block content %}
+<h1>{{ object }}</h1>
+
+<p>{{  object.description|linebreaksbr }}</p>
+
+<h2>Przykład</h2>
+<pre style="border:1px solid black;padding: 1em; margin: 2em;">{{ object.example }}</pre>
+
+
+<h2>HTML</h2>
+<iframe src="{{ object.example_html.url }}" style="width: 100%; height: 500px;"></iframe>
+
+<h2>PDF</h2>
+<iframe src="{{ object.example_pdf.url }}" style="width: 100%; height: 500px;"></iframe>
+
+<h2>TXT</h2>
+<iframe src="{{ object.example_txt.url }}" style="width: 100%; height: 500px;"></iframe>
+
+
+{% endblock %}
diff --git a/src/wlxml/templates/wlxml/tag_list.html b/src/wlxml/templates/wlxml/tag_list.html
new file mode 100644 (file)
index 0000000..b9a9c27
--- /dev/null
@@ -0,0 +1,5 @@
+<ul>
+{% for tag in object_list %}
+  <li><a href="{{ tag.get_absolute_url }}">{{ tag }}</li>
+{% endfor %}
+</ul>
diff --git a/src/wlxml/templates/wlxml/wl2html.xsl b/src/wlxml/templates/wlxml/wl2html.xsl
new file mode 100644 (file)
index 0000000..91d41c4
--- /dev/null
@@ -0,0 +1,291 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0"    
+    xmlns="http://www.w3.org/1999/xhtml"    
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+    <xsl:output method="html" omit-xml-declaration="yes" encoding="utf-8" indent="no" />
+
+    <!--
+        Base tag for rendering a fragment of text
+    -->
+    <xsl:template match="chunk">        
+        <xsl:apply-templates select="child::node()">            
+            <xsl:with-param name="mixed" select="true()" />
+        </xsl:apply-templates>        
+    </xsl:template>
+    
+    <xsl:template match="utwor">
+        <div>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="false()" />
+            </xsl:apply-templates>
+        </div>
+    </xsl:template>    
+
+
+    <!-- nieedytowany - zawiera bloki   section-->
+    <xsl:template match="{{ tags.section|join:"|" }}">
+        <xsl:param name="mixed" />
+        <div>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="false()" />
+            </xsl:apply-templates>
+        </div>
+    </xsl:template>
+
+    <!-- edytowalny - zawiera tekst      div -->
+    <xsl:template match="{{ tags.div|join:"|" }}">
+        <xsl:param name="mixed" />
+        <div x-editable="true">
+            <xsl:call-template name="standard-attributes" />
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="true()" />
+            </xsl:apply-templates>
+        </div>
+    </xsl:template>
+    
+    <xsl:template match="ilustr">
+      <div>
+        <xsl:call-template name="standard-attributes" />
+        <img>
+          <xsl:attribute name="src">
+            <xsl:value-of select="@src" />
+          </xsl:attribute>
+        </img>
+        <p class="alt"><xsl:value-of select="@alt"/></p>
+      </div>
+    </xsl:template>
+
+    <!--
+        ********
+        STROFA
+        ********
+    -->
+    <xsl:template match="strofa">
+        <div x-editable="true">
+            <xsl:call-template name="standard-attributes" />
+         
+            <xsl:choose>
+                <xsl:when test="count(br) > 0">
+                    <xsl:variable name="first-verse" select="br[1]/preceding-sibling::node()" />                    
+                    <xsl:call-template name="verse">
+                        <xsl:with-param name="verse-content" select="$first-verse" />                        
+                    </xsl:call-template>
+                    <xsl:for-each select="br">
+                        <xsl:variable name="lnum" select="count(preceding-sibling::br)" />
+                        <!-- select all nodes up to the next br or end of stanza -->
+                        <xsl:variable name="current-verse"
+                            select="following-sibling::node()[count(preceding-sibling::br) = $lnum+1]" />                        
+                        <xsl:call-template name="verse">
+                            <xsl:with-param name="verse-content" select="$current-verse" />                            
+                        </xsl:call-template>
+                    </xsl:for-each>
+                </xsl:when>
+                <xsl:otherwise>
+                    <xsl:call-template name="verse">
+                        <xsl:with-param name="verse-content" select="child::node()" />                        
+                    </xsl:call-template>
+                </xsl:otherwise>
+            </xsl:choose>
+        </div>
+    </xsl:template>
+
+    <xsl:template name="verse">
+        <!-- the verse contents including the last br (if any) -->
+        <xsl:param name="verse-content" />
+        <xsl:variable name="first-tag-name" select="name($verse-content/self::*)" />
+        <!-- name of text nodes is '' == false -->
+
+        <!-- THIS IS A HORROR!!! -->
+        <!-- Possible variants: -->
+        <xsl:choose>
+            <!-- Simple verse == not wers_ tags anywhere until the ending br -->
+            <xsl:when test="not($verse-content[starts-with(name(), 'wers')])">
+                <div class="wers" x-node="wers" x-verse="true" x-auto-node="true">
+                <xsl:apply-templates select="$verse-content[local-name(.) != 'br']">
+                    <xsl:with-param name="mixed" select="true()" />
+                </xsl:apply-templates>
+                </div>
+            </xsl:when>
+
+            <xsl:otherwise>
+            <xsl:apply-templates select="$verse-content[local-name(.) != 'br']">
+                <xsl:with-param name="mixed" select="false()" />
+            </xsl:apply-templates>
+            </xsl:otherwise>
+        </xsl:choose>
+    </xsl:template>
+
+    {% if tags.verse %}
+    <xsl:template match="{{ tags.verse|join:"|" }}">
+        <xsl:param name="mixed" />
+               <div x-verse="true">
+               <xsl:call-template name="standard-attributes" />
+               <xsl:apply-templates select="child::node()">
+                       <xsl:with-param name="mixed" select="true()" />
+               </xsl:apply-templates>
+               </div>
+    </xsl:template>
+    {% endif %}
+
+    <xsl:template match="br"><xsl:text>/</xsl:text></xsl:template>
+
+    {% if tags.span %}
+    <!-- Style znakowe         span -->
+    <xsl:template match="{{ tags.span|join:"|" }}">
+        <xsl:param name="mixed" />
+        <em>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="true()" />
+            </xsl:apply-templates>
+        </em>
+    </xsl:template>
+    {% endif %}
+
+    {% if tags.sep %}
+    <!-- Separatory            sep -->
+    <xsl:template match="{{ tags.sep|join:"|" }}">
+        <xsl:param name="mixed" />
+        <hr><xsl:call-template name="standard-attributes" /></hr>
+    </xsl:template>
+    {% endif %}
+
+    {% if tags.aside %}
+    <!-- Przypisy i motywy     aside -->
+    <xsl:template match="{{ tags.aside|join:"|" }}">
+        <span x-editable="true">
+            <xsl:call-template name="standard-attributes">
+                <xsl:with-param name="extra-class" select="'annotation-inline-box'" />
+            </xsl:call-template>
+            <a name="anchor-{generate-id(.)}" />
+            <!-- the link to the non-inline version -->
+            <a href="#annotation-{generate-id(.)}" class="annotation"></a>
+            <!-- inline contents -->
+            <span x-annotation-box="true" x-pass-thru="true">
+                <xsl:apply-templates select="node()">
+                    <xsl:with-param name="mixed" select="true()" />
+                </xsl:apply-templates>
+            </span>
+        </span>
+    </xsl:template>
+    {% endif %}
+
+    <xsl:template match="ref">
+      <span x-editable="true" x-edit-no-format="true" x-edit-attribute="href">
+        <xsl:call-template name="standard-attributes">
+          <xsl:with-param name="extra-class" select="'reference-inline-box'" />
+        </xsl:call-template>
+        <a class="reference">📌</a>
+      </span>
+    </xsl:template>
+    
+    <xsl:template match="begin">        
+        <span>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:attribute name="theme-class">
+                <xsl:value-of select="substring-after(@id, 'b')" />
+            </xsl:attribute>
+        </span>
+    </xsl:template>
+
+    <xsl:template match="motyw">
+        <span x-editable="true" x-edit-no-format="true">
+            <xsl:call-template name="standard-attributes" />
+            <xsl:attribute name="theme-class">
+                <xsl:value-of select="substring-after(@id, 'm')" />
+            </xsl:attribute>
+            <span x-pass-thru="true" class="theme-text-list"><xsl:value-of select=".|node()" /></span>
+        </span>
+    </xsl:template>
+
+    <xsl:template match="end">
+        <span>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:attribute name="theme-class">
+                <xsl:value-of select="substring-after(@id, 'e')" />
+            </xsl:attribute>
+        </span>
+    </xsl:template>
+
+
+    <!-- Tekst -->
+    <xsl:template match="text()">
+      <!-- <xsl:value-of select="." /> -->
+      <xsl:param name="mixed" />
+      <xsl:choose>
+        <xsl:when test="normalize-space(.) = ''">
+          <xsl:value-of select="." />
+        </xsl:when>
+        <xsl:when test="not($mixed)">
+          <span x-node="out-of-flow-text" class="out-of-flow-text" x-editable="true">
+            <xsl:value-of select="." />
+         </span>
+       </xsl:when>
+        <xsl:otherwise><xsl:value-of select="." /></xsl:otherwise>
+      </xsl:choose>
+    </xsl:template>
+
+    <xsl:template match="comment()">
+        <xsl:comment><xsl:value-of select="." /></xsl:comment>
+    </xsl:template>
+
+    <xsl:template match="*[name() != local-name()]">
+        <span>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="true()" />
+            </xsl:apply-templates>
+        </span>
+    </xsl:template>
+       
+    <xsl:template match="*">
+        <span>
+            <xsl:call-template name="standard-attributes">
+                <xsl:with-param name="extra-class">unknown-tag</xsl:with-param>
+            </xsl:call-template>
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="true()" />
+            </xsl:apply-templates>        
+        </span>
+    </xsl:template>
+
+    <xsl:template name="standard-attributes">
+        <xsl:param name="extra-class" />
+        <xsl:attribute name="class"><xsl:value-of select="$extra-class" /></xsl:attribute>
+
+        <xsl:attribute name="x-node"><xsl:value-of select="local-name()" /></xsl:attribute>
+
+        <xsl:if test="local-name() != name()">
+            <xsl:attribute name="x-ns"><xsl:value-of select="namespace-uri()" /></xsl:attribute>
+        </xsl:if>
+
+        <xsl:for-each select="@*">
+            <xsl:variable name="id" select="generate-id()" />
+            <xsl:attribute name="x-attr-value-{$id}"><xsl:value-of select="."/></xsl:attribute>
+            <xsl:attribute name="x-attr-name-{$id}"><xsl:value-of select="local-name()"/></xsl:attribute>
+            <xsl:choose>
+               <xsl:when test="namespace-uri()">
+                <xsl:attribute name="x-attr-ns-{$id}"><xsl:value-of select="namespace-uri()"/></xsl:attribute>
+                               </xsl:when>
+                               <!-- if the element belongs to default namespace and attribut has no namespace -->
+               <xsl:when test="not(namespace-uri(.))">
+                               <xsl:attribute name="data-wlf-{local-name()}"><xsl:value-of select="."/></xsl:attribute>
+                               </xsl:when>
+                       </xsl:choose>               
+        </xsl:for-each>
+    </xsl:template>
+
+    <xsl:template match="alien">
+            <span class="alien" x-pass-thru="true">
+                <xsl:apply-templates select="node()">
+                    <xsl:with-param name="mixed" select="true()" />
+                </xsl:apply-templates>
+            </span>
+    </xsl:template>
+
+    <xsl:template match="comment()">
+        <xsl:comment><xsl:value-of select="."/></xsl:comment>
+    </xsl:template>
+</xsl:stylesheet>
diff --git a/src/wlxml/urls.py b/src/wlxml/urls.py
new file mode 100644 (file)
index 0000000..852bed1
--- /dev/null
@@ -0,0 +1,11 @@
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('wl2html.xsl', views.XslView.as_view()),
+    path('wl.css', views.EditorCSS.as_view()),
+
+    path('tags/', views.TagsView.as_view(), name='wlxml_tags'),
+    path('tags/<slug>/', views.TagView.as_view(), name='wlxml_tag'),
+]
diff --git a/src/wlxml/views.py b/src/wlxml/views.py
new file mode 100644 (file)
index 0000000..9a2a046
--- /dev/null
@@ -0,0 +1,34 @@
+from io import BytesIO
+from django.views.generic import TemplateView, ListView, DetailView
+from . import models
+from librarian.document import WLDocument
+from librarian.builders import StandaloneHtmlBuilder
+
+
+class XslView(TemplateView):
+    template_name = 'wlxml/wl2html.xsl'
+    content_type = 'application/xslt+xml'
+
+    def get_context_data(self):
+        ctx = super().get_context_data()
+        tags = {}
+        for t in models.Tag.objects.all():
+            tags.setdefault(t.type, []).append(t.name)
+        ctx['tags'] = tags
+        return ctx
+
+
+class EditorCSS(ListView):
+    template_name = 'wlxml/editor.css'
+    content_type = 'text/css'
+    queryset = models.Tag.objects.all()
+        
+
+class TagsView(ListView):
+    queryset = models.Tag.objects.all()
+
+
+class TagView(DetailView):
+    queryset = models.Tag.objects.all()
+    slug_field = 'name'
+