src/lexml/exportarParaLexML.js
/* Copyright 2017 Assembleia Legislativa de Minas Gerais
*
* This file is part of Editor-Articulacao.
*
* Editor-Articulacao is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* Editor-Articulacao is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Editor-Articulacao. If not, see <http://www.gnu.org/licenses/>.
*/
import ArticulacaoInvalidaException from './ArticulacaoInvalidaException';
import padrao from '../opcoesPadrao';
var htmlInline = new Set(['SPAN', 'B', 'I', 'A', 'SUB', 'SUP', 'INS', 'DEL', 'DFN']);
/**
* Exporta para o XML definido no LexML (http://projeto.lexml.gov.br/documentacao/Parte-3-XML-Schema.pdf),
* a partir da estrutura do HTML do editor de articulação.
*
* @param {Element} dispositivoDOM Dispositivo da articulação, dentro do editor de articulação,
* onde será iniciada a exportação para LexML, ou o próprio container (elemento raíz) do
* editor de articulação.
* @returns {Element} Articulação do LexML
*/
function exportarParaLexML(dispositivoDOM, rotulos) {
var cntArtigos = 0;
if (!rotulos) {
rotulos = padrao.rotulo;
}
/**
* Representa o contexto da transformação,
* com informações e métodos para adição de dispositivos,
* podendo ser especializado por tipo.
*/
class ContextoTransformacao {
constructor(dispositivoLexML, contextoAnterior) {
this.dispositivoLexML = dispositivoLexML;
this.contextoAnterior = contextoAnterior;
this.cntSubitens = {
get Artigo() {
return cntArtigos;
},
set Artigo(valor) {
cntArtigos = valor;
}
};
}
get id() {
return this.dispositivoLexML.id;
}
/**
* Obtém o identificador a ser considerado ao criar o id para
* um subtipo deste dispositivo.
*
* @param {String} subtipo Subtipo desejado.
*/
getIdReferencia(subtipo) {
return this.dispositivoLexML.id;
}
/**
* Tipo do dispositivo.
*/
get tipo() {
return this.dispositivoLexML.tagName;
}
adicionarSubitem(dispositivoLexML) {
this.cntSubitens[dispositivoLexML.tagName] = this.cntSubitens[dispositivoLexML.tagName] ? this.cntSubitens[dispositivoLexML.tagName] + 1 : 1;
this.dispositivoLexML.appendChild(dispositivoLexML);
}
adicionarSubitemEmendado(dispositivoLexML) {
this.dispositivoLexML.appendChild(dispositivoLexML);
}
adicionarProximo(dispositivoLexML) {
this.dispositivoLexML.parentNode.appendChild(dispositivoLexML);
}
possuiSubtipo(subtipo) {
switch (this.tipo) {
case 'Articulacao':
return subtipo === 'Artigo' || subtipo === 'Titulo' || subtipo === 'Capitulo' || subtipo === 'Secao' || subtipo === 'Livro' || subtipo === 'Parte';
case 'Parte':
return subtipo === 'Livro';
case 'Livro':
return subtipo === 'Titulo';
case 'Titulo':
return subtipo === 'Capitulo' || subtipo === 'Artigo';
case 'Capitulo':
return subtipo === 'Secao' || subtipo === 'Artigo';
case 'Secao':
return subtipo === 'Subsecao' || subtipo === 'Artigo';
case 'Subsecao':
return subtipo === 'Artigo';
case 'Artigo':
return subtipo === 'Inciso' || subtipo === 'Paragrafo';
case 'Inciso':
return subtipo === 'Alinea';
case 'Alinea':
return subtipo === 'Item';
case 'Item':
return false;
case 'Paragrafo':
return subtipo === 'Inciso';
default:
throw 'Tipo desconhecido: ' + this.tipo;
}
}
contarSubitens(subtipo) {
return this.cntSubitens[subtipo] || 0;
}
}
/**
* Contexto de transformação especializado para artigo, que possui
* incisos no caput e parágrafos no artigo.
*/
class ContextoTransformacaoArtigo extends ContextoTransformacao {
getIdReferencia(subtipo) {
// O inciso é sobre o caput. Já o parágrafo é sobre o artigo.
return subtipo === 'Inciso' ? this.dispositivoLexML.id + '_cpt' : this.dispositivoLexML.id;
}
adicionarSubitem(dispositivoLexML) {
if (dispositivoLexML.tagName === 'p' || dispositivoLexML.tagName === 'Inciso') {
this.dispositivoLexML.querySelector('Caput').appendChild(dispositivoLexML);
} else {
this.dispositivoLexML.appendChild(dispositivoLexML);
}
this.cntSubitens[dispositivoLexML.tagName] = this.cntSubitens[dispositivoLexML.tagName] ? this.cntSubitens[dispositivoLexML.tagName] + 1 : 1;
}
}
/**
* Contexto de transformação especializado em divisões de texto (agrupadores),
* como título, capítulo, seção e subseção.
*/
class ContextoTransformacaoAgrupador extends ContextoTransformacao {
getIdReferencia(subtipo) {
return subtipo === 'Artigo' ? null : super.getIdReferencia(subtipo);
}
}
/**
* Cria o contexto adequado conforme o tipo do nó.
*
* @param {Element} dispositivoLexML
* @param {*} contextoAnterior
*/
function criarContexto(dispositivoLexML, contextoAnterior) {
switch (dispositivoLexML.tagName) {
case 'Artigo':
return new ContextoTransformacaoArtigo(dispositivoLexML, contextoAnterior);
case 'Parte':
case 'Titulo':
case 'Capitulo':
case 'Secao':
case 'Subsecao':
return new ContextoTransformacaoAgrupador(dispositivoLexML, contextoAnterior);
default:
return new ContextoTransformacao(dispositivoLexML, contextoAnterior);
}
}
let raiz = document.createElementNS('http://www.lexml.gov.br/1.0', 'Articulacao');
let contexto = new ContextoTransformacao(raiz);
// Se for o container de edição, então movemos para o primeiro filho.
if (dispositivoDOM.classList.contains('silegismg-editor-articulacao')) {
dispositivoDOM = dispositivoDOM.firstElementChild;
}
while (dispositivoDOM) {
let tipo = dispositivoDOM.getAttribute('data-tipo').replace(/^[a-z]/, letra => letra.toUpperCase());
if (!tipo) {
throw new ArticulacaoInvalidaException(dispositivoDOM, 'Dispositivo não possui tipo definido.');
} else if (tipo === 'Continuacao') {
if (!contexto.contextoAnterior) {
throw new ArticulacaoInvalidaException(dispositivoDOM, 'Continuação não possui dispositivo anterior.');
}
let p = criarElementoP(dispositivoDOM);
contexto.adicionarSubitem(p);
} else {
while (contexto && !contexto.possuiSubtipo(tipo)) {
contexto = contexto.contextoAnterior;
}
if (!contexto) {
throw new ArticulacaoInvalidaException(dispositivoDOM, 'Dispositivo do tipo "' + tipo.toLowerCase() + '" inesperado neste ponto.');
}
if (dispositivoDOM.classList.contains('emenda')) {
contexto.nEmenda++;
} else {
contexto.nEmenda = 0;
}
let dispositivoLexML = criarElementoLexML(tipo, dispositivoDOM, contexto.getIdReferencia(tipo), contexto.contarSubitens(tipo), dispositivoDOM.classList.contains('unico'), contexto.nEmenda, rotulos);
if (tipo === 'Paragrafo' && dispositivoDOM.classList.contains('unico')) {
// Adiciona o sufixo "u" ao identificador do parágrafo único
dispositivoLexML.id = dispositivoLexML.id + 'u';
}
if (contexto.nEmenda) {
contexto.adicionarSubitemEmendado(dispositivoLexML);
} else {
contexto.adicionarSubitem(dispositivoLexML);
}
contexto = criarContexto(dispositivoLexML, contexto);
}
dispositivoDOM = dispositivoDOM.nextElementSibling;
}
return raiz;
}
function criarElementoLexML(tipo, conteudo, idPai, idxFilho, unico, nEmenda, rotulos) {
var elemento, id;
id = tipo.substr(0, 3).toLowerCase();
id = idPai ? idPai + '_' + id : id;
if (nEmenda) {
id += idxFilho + '-' + nEmenda;
} else {
id += idxFilho + 1;
}
elemento = document.createElementNS('http://www.lexml.gov.br/1.0', tipo);
elemento.setAttribute('id', id);
elemento.appendChild(criarRotuloLexML(tipo, nEmenda ? idxFilho : idxFilho + 1, unico, nEmenda, rotulos));
switch (tipo) {
case 'Artigo':
elemento.appendChild(criarCaputLexML(conteudo, id));
break;
// Agrupadores
case 'Parte':
case 'Livro':
case 'Titulo':
case 'Capitulo':
case 'Secao':
case 'Subsecao':
elemento.appendChild(criarNomeAgrupador(conteudo));
break;
default:
elemento.appendChild(criarElementoP(conteudo));
break;
}
return elemento;
}
function criarRotuloLexML(tipo, numero, unico, nEmenda, rotulos) {
var elemento = document.createElementNS('http://www.lexml.gov.br/1.0', 'Rotulo');
switch (tipo) {
case 'Artigo':
if (nEmenda) {
elemento.innerHTML = 'Art. ' + numero + (numero < 10 ? 'º-' : '-') + transformarLetra(nEmenda, true) + (numero < 10 ? rotulos.separadorArtigo : rotulos.separadorArtigoSemOrdinal);
} else {
elemento.innerHTML = 'Art. ' + numero + (numero < 10 ? 'º' + rotulos.separadorArtigo : rotulos.separadorArtigoSemOrdinal);
}
break;
case 'Paragrafo':
elemento.innerHTML = unico ? 'Parágrafo único' + rotulos.separadorParagrafoUnico : '§ ' + numero + (numero < 10 ? 'º' + rotulos.separadorParagrafo : rotulos.separadorParagrafoSemOrdinal);
break;
case 'Inciso':
elemento.innerHTML = transformarNumeroRomano(numero) + rotulos.separadorInciso;
break;
case 'Alinea':
elemento.textContent = transformarLetra(numero) + rotulos.separadorAlinea;
break;
case 'Item':
elemento.innerHTML = numero + rotulos.separadorItem;
break;
case 'Titulo':
elemento.textContent = 'Título ' + transformarNumeroRomano(numero);
break;
case 'Capitulo' :
elemento.textContent = 'Capítulo ' + transformarNumeroRomano(numero);
break;
case 'Secao':
elemento.textContent = 'Seção ' + transformarNumeroRomano(numero);
break;
case 'Subsecao':
elemento.textContent = 'Subseção ' + transformarNumeroRomano(numero);
break;
default:
throw "Tipo não suportado na formatação de rótulo: " + tipo;
}
return elemento;
}
function criarCaputLexML(caput, idPai) {
var elemento = document.createElementNS('http://www.lexml.gov.br/1.0', 'Caput');
var paragrafo = criarElementoP(caput);
elemento.setAttribute('id', idPai + '_cpt');
elemento.appendChild(paragrafo);
normalizarParagrafo(paragrafo);
return elemento;
}
function criarElementoP(paragrafo) {
var elemento = document.createElementNS('http://www.lexml.gov.br/1.0', 'p');
criarConteudoInline(paragrafo, elemento);
normalizarParagrafo(elemento);
return elemento;
}
function criarConteudoInline(origem, destino) {
var arvore = document.createTreeWalker(origem, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null /* necesário no IE 11 */, null /* necesário no IE 11 */);
var atual = {
/**
* Objeto de contexto de exportação anterior (este mesmo objeto)
*/
anterior: null,
/**
* Elemento que, se encontrado, deve retroceder o contexto. Utilizado para navegar
* por elementos filhos e identificar quando o nível, na navegação, retrocede,
* obrigando a retroceder também o contexto.
*/
retrocederEm: null,
/**
* Elemento na nova árvore onde os itens serão inseridos.
*/
destino: destino
};
var primeiroTexto = true, ultimoTexto = null;
while (arvore.nextNode()) {
let item = arvore.currentNode;
if (item.parentNode === atual.retrocederEm) {
atual = atual.anterior;
}
if (item.nodeType === Node.TEXT_NODE) {
ultimoTexto = item.cloneNode();
if (primeiroTexto) {
primeiroTexto = false;
ultimoTexto.textContent = ultimoTexto.textContent.replace(/^\s+/, '');
}
atual.destino.appendChild(ultimoTexto);
} else if (htmlInline.has(item.tagName)) {
let elemento = document.createElementNS('http://www.lexml.gov.br/1.0', item.tagName.toLowerCase());
atual.destino.appendChild(elemento);
atual = {
anterior: atual,
retrocederEm: item.parentNode,
destino: elemento
};
} else if (item.tagName === 'BR') {
// Ignora
} else {
throw new ArticulacaoInvalidaException(origem, 'Elemento não permitido: ' + item.tagName);
}
}
if (ultimoTexto) {
ultimoTexto.textContent = ultimoTexto.textContent.replace(/\s+$/, '');
}
return destino;
}
function criarNomeAgrupador(elementoAgrupador) {
var elemento = document.createElementNS('http://www.lexml.gov.br/1.0', 'NomeAgrupador');
criarConteudoInline(elementoAgrupador, elemento);
return elemento;
}
/**
* Desmembra elementos P dentro de outro P, colocando-os no mesmo nível.
* Exemplo: `<P><P>1</P><P>2</P></P>` vira `<P>1</P><P>2</P>`
*
* @param {Element} paragrafo
*/
function normalizarParagrafo(paragrafo) {
while (paragrafo.hasAttributes()) {
paragrafo.removeAttribute(paragrafo.attributes[0].name);
}
/* Microsoft Edge não implementa a interface HTMLElement em document.createElementNS.
* Portanto, usaremos children se disponível, ou childNodes, para o Edge.
*/
let itens = paragrafo.children || paragrafo.childNodes;
for (let i = 0; i < itens.length; i++) {
if (itens[i].tagName === 'p') {
/* Tag P dentro de P. Isso não deveria ocorrer, então vamos tratar como continuação.
* Neste caso, move-se todos os elementos a partir do índice 'i'
* para novos parágrafos após o atual.
*/
while (itens.length > i) {
let item = itens[i];
if (item.tagName === 'p') {
paragrafo.parentNode.insertBefore(item, paragrafo.nextSibling);
normalizarParagrafo(item);
} else {
var novoParagrafo = document.createElementNS('http://www.lexml.gov.br/1.0', 'p');
novoParagrafo.appendChild(item);
paragrafo.parentNode.insertBefore(novoParagrafo, paragrafo.nextSibling);
normalizarParagrafo(novoParagrafo);
}
}
}
}
}
/**
* Escreve o número em romano.
*
* @param {Number} numero
*/
function transformarNumeroRomano(numero) {
var digitos = String(numero).split(""),
tabela = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM",
"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC",
"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"],
resultado = "",
i = 3;
while (i--) {
resultado = (tabela[+digitos.pop() + (i * 10)] || "") + resultado;
}
return new Array(+digitos.join("") + 1).join("M") + resultado;
}
/**
* Escreve o número em letra.
*
* @param {Number} numero
*/
function transformarLetra(numero, maiuscula) {
if (numero < 1) {
throw "Número deve ser positivo.";
}
return String.fromCharCode((maiuscula ? 64 : 96) + numero);
}
export default exportarParaLexML;