From 50b8a8fccc8876f2916e53aac46726c97a6356f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20PENHO=C3=8BT?= Date: Wed, 29 Sep 2021 17:56:10 +0200 Subject: [PATCH] =?UTF-8?q?Nouvelle=20version=20avec=20classe=20s=C3=A9par?= =?UTF-8?q?=C3=A9e=20pour=20parser=20le=20CSV.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/errors.js | 8 +- src/firstExample.ts | 13 +-- src/freeDatas2HTML.ts | 152 +++++++++++++----------------- src/freeDatas2HTMLInterfaces.ts | 30 ++++++ src/freeDatas2HTMLPagination.ts | 2 +- src/freeDatas2HTMLParserForCSV.ts | 129 +++++++++++++++++++++++++ src/freeDatas2HTMLRender.ts | 8 +- src/freeDatas2HTMLSelector.ts | 10 +- src/freeDatas2HTMLSortingField.ts | 4 +- src/papaParseInterfaces.ts | 23 ----- 11 files changed, 251 insertions(+), 130 deletions(-) create mode 100644 src/freeDatas2HTMLParserForCSV.ts delete mode 100644 src/papaParseInterfaces.ts diff --git a/package.json b/package.json index d48d007..0e53ebd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freedatas2html", - "version": "0.5.2", + "version": "0.6.0", "description": "Visualization of data from various sources (CSV, API, HTML...) with filters, classification, pagination, etc.", "main": "index.js", "scripts": { diff --git a/src/errors.js b/src/errors.js index 31e39eb..ac6b787 100644 --- a/src/errors.js +++ b/src/errors.js @@ -3,6 +3,7 @@ module.exports = converterElementNotFound : "Aucun élément HTML n'a été trouvé ayant comme \"id\" : ", converterFieldNotFound : "Le champ n'existe pas dans les données ou les données n'ont pas encore été chargées.", converterNeedDatasElt: "Merci de fournir un id valide pour l'élément où afficher les données.", + converterNeedDatas: "Merci de fournir les données à traiter.", converterRefreshFail: "Le nom des champs et l'élement du DOM receveur sont nécessaires à l'affichage des données.", pagination2HTMLFail : "Toutes les donnée nécessaires à la création des sélecteurs de pagination n'ont pas été fournies.", paginationNeedByfaultValueBeInOptions: "La valeur de pagination par défaut doit faire partie des options proposées.", @@ -10,8 +11,11 @@ module.exports = paginationNeedOptionsValues: "Vous n'avez fourni aucune options possibles pour la pagination.", paginationNeedPositiveInteger: "Merci de fournir un nombre entier supérieur à zéro pour désigner chaque option de pagination.", parserDatasNotFound : "Aucune donnée n'a été trouvée.", - parserFail: "La lecture des données du fichier a échoué.", - parserNeedUrl: "Merci de fournir une url valide pour le fichier à parser.", + parserFail: "La lecture des données a échoué.", + parserMeetErrors : "Au moins une erreur a été rencontrée durant le traitement des données.", + parserNeedDatas: "Merci de fournir une chaîne de caractères valide à parser.", + parserNeedSource: "Merci de fournir une chaîne de caractères où une url pour les données à parser.", + parserNeedUrl: "Merci de fournir une url valide pour la source distante de données.", renderNeedDatas: "Il ne peut y avoir de pagination, si les données n'ont pas été récupérées.", renderUnknownField: "Un champ non attendu a été trouvé dans les données à afficher : ", selector2HTMLFail: "Le création d'un filtre dans le DOM nécessite l'initialisation de l'élément HTML et du numéro du champs à filter.", diff --git a/src/firstExample.ts b/src/firstExample.ts index 47ccb21..8d242b1 100644 --- a/src/firstExample.ts +++ b/src/firstExample.ts @@ -19,11 +19,9 @@ const initialise = async () => }; // Création d'un convertisseur parsant les données d'un fichier CSV "distant" - let converter=new FreeDatas2HTML(); + let converter=new FreeDatas2HTML("CSV","", { url:"http://localhost:8080/datas/elements-chimiques.csv"}); converter.datasViewElt={ id:"datas" }; - converter.datasSourceUrl="http://localhost:8080/datas/elements-chimiques.csv"; - await converter.parse(); - converter.datasSortingFunctions=[{ datasFieldNb:4, sort:mySort }]; + await converter.run(); // Adaptation du rendu suivant la taille de l'écran const myRender=new Render(converter); if(window.innerWidth < 800) @@ -46,6 +44,9 @@ const initialise = async () => converter.datasRender=myRender; } + // Ajout d'une fonction de classement spécifique + converter.datasSortingFunctions=[{ datasFieldNb:4, sort:mySort }]; + // Configuration de la pagination const pagination=new Pagination(converter, { id:"pages" }, "Page à afficher :"); pagination.options={ displayElement: { id:"paginationOptions" }, values: [10,20,50,500] , name: "Choix de pagination :" }; @@ -55,7 +56,7 @@ const initialise = async () => // Affichage initial converter.datasCounter={ id:"compteur" }; - await converter.run(); + await converter.run(); // Création d'outils permettant de filtrer les données des champs de données let filtre1=new Selector(converter, 3, { id:"filtre1"} ); @@ -67,7 +68,7 @@ const initialise = async () => filtre3.selector2HTML(); // + Injection des filtres dans le convertisseur converter.datasSelectors=[filtre1,filtre2,filtre3]; - + // Ajout de champs permettant de classer les données // Uniquement avec un rendu tableau (grand écran), car entêtes de colonne nécessaires if(window.innerWidth >= 800) diff --git a/src/freeDatas2HTML.ts b/src/freeDatas2HTML.ts index ce5acfc..cad03f3 100644 --- a/src/freeDatas2HTML.ts +++ b/src/freeDatas2HTML.ts @@ -1,52 +1,77 @@ -const Papa = require("papaparse"); -const errors = require("./errors.js"); -const { compare }= require('natural-orderby'); +const { compare }=require('natural-orderby'); +const errors=require("./errors.js"); -import { Counter, DatasRenders, DOMElement, Paginations, Selectors, SortingFields, SortingFunctions } from "./freeDatas2HTMLInterfaces"; +import { Counter, Datas, DatasRenders, DOMElement, Paginations, Parsers, ParseErrors, RemoteSource, Selectors, SortingFields, SortingFunctions } from "./freeDatas2HTMLInterfaces"; import { Pagination} from "./freeDatas2HTMLPagination"; +import { ParserForCSV} from "./freeDatas2HTMLParserForCSV"; import { Render} from "./freeDatas2HTMLRender"; import { Selector } from "./freeDatas2HTMLSelector"; import { SortingField } from "./freeDatas2HTMLSortingField"; -import { PapaParseDatas, PapaParseErrors, PapaParseMeta } from "./papaParseInterfaces"; - export class FreeDatas2HTML { // L'élément HTML où afficher les données. Laisser à undefined si non affichées : - private _datasViewElt: DOMElement|undefined = undefined; + private _datasViewElt: DOMElement|undefined=undefined; // Le moteur de rendu pour préparer l'affichage des données public datasRender: DatasRenders; // Le code HTML résultant : - public datasHTML: string = ""; + public datasHTML: string = ""; + // Le parseur : + public parser: Parsers; // public pour permettre de charger un parseur tiers après instanciation - // L'url où accéder aux données : - private _datasSourceUrl: string = ""; - // Le nom des champs (interfaces à renommer, car PapaParse = cas particulier) : - public parseMetas: PapaParseMeta|undefined = undefined; + // Données distantes : + //private _datasRemoteSource: RemoteSource|undefined=undefined; + // Ou locales : + //private _datas2Parse:string|undefined=undefined; + // Dans tous les cas, besoin d'un type : + public datasType: "CSV"|"HTML"|"JSON"|undefined; + + // Le nom des champs trouvés dans les données : + public fields: string[]|undefined=undefined; // Les données à proprement parler : - public parseDatas: PapaParseDatas[] = []; - // Les erreurs rencontrées durant le parsage : - public parseErrors: PapaParseErrors[] = []; - // Doit-on tout arrêter si une erreur est rencontrée durant la parsage ? + public datas: []=[]; + // Les erreurs rencontrées durant le traitement des données reçues : + public parseErrors: ParseErrors[]|undefined; + // Doit-on tout arrêter si une erreur est rencontrée durant le traitement ? public stopIfParseErrors: boolean = false; // Les fonctions spécifiques de classement pour certains champs : private _datasSortingFunctions: SortingFunctions[] = []; - // Les filtres possible sur certains champs + // Les filtres possible sur certains champs : datasSelectors: Selectors[] = []; - // Les champs pouvant être classés + // Les champs pouvant être classés : datasSortingFields: SortingFields[] = []; - // La dernier champ pour lequel le classement a été demandé + // La dernier champ pour lequel le classement a été demandé : datasSortedField: SortingFields|undefined; - // La Pagination : + // Éventuelle pagination : pagination: Paginations|undefined; // Affichage du nombre total de lignes de données (optionnel) : private _datasCounter: Counter = {}; // J'initialiser avec des valeurs par défaut pouvant être surchargées par les setters - constructor() + constructor(datasType:"CSV"|"HTML"|"JSON", datas2Parse="", datasRemoteSource?:RemoteSource) { this.datasRender=new Render(this); + switch (datasType) + { + case "CSV": + this.parser=new ParserForCSV(); + break; + case "HTML": + this.parser=new ParserForCSV(); + console.error("Appeler le parseur HTML"); + break; + case "JSON": + this.parser=new ParserForCSV(); + console.error("Appeler le parseur JSON"); + break; + } + if(datas2Parse !== "") + this.parser.datas2Parse=datas2Parse; + else if(datasRemoteSource!==undefined) + this.parser.datasRemoteSource=datasRemoteSource; + else + throw new Error(errors.converterNeedDatas); } // Vérifie s'il y a bien un élément dans le DOM pour l'id fourni @@ -65,7 +90,7 @@ export class FreeDatas2HTML // Vérifie qu'un champ existe bien dans les données public checkFieldExist(nb: number) : boolean { - if(this.parseMetas === undefined || this.parseMetas.fields === undefined || this.parseMetas.fields[nb] === undefined) + if(this.fields === undefined || this.fields[nb] === undefined) return false; else return true; @@ -77,15 +102,6 @@ export class FreeDatas2HTML this._datasViewElt=FreeDatas2HTML.checkInDOMById(elt); } - // Vérifie que l'url où chercher les données n'est pas vide : inutile si données dans page ou tranmises - set datasSourceUrl(url: string) - { - if(url.trim().length === 0) - throw new Error(errors.parserNeedUrl); - else - this._datasSourceUrl=url.trim(); - } - // Vérifie que les numéros de champs pour lesquels il y a des fonctions de classement spécifiques sont cohérents // ! Ne peut être testé qu'après avoir reçu les données set datasSortingFunctions(SortingFunctions: SortingFunctions[]) @@ -126,72 +142,36 @@ export class FreeDatas2HTML return undefined; } - // Parse des données distantes (url) fournies en CSV : - public async parse(): Promise - { - const converter=this; - return new Promise((resolve,reject) => - { - if(converter._datasSourceUrl !== "" ) - { - Papa.parse(converter._datasSourceUrl, - { - quoteChar: '"', - header: true, - complete: function(results :any) - { - converter.parseErrors=results.errors; - converter.parseDatas=results.data; - // Attention, papaParse peut accepter un nom de colonne vide - let realFields: string[]=[]; - for(let i in results.meta.fields) - { - if(results.meta.fields[i].trim() !== "") - realFields.push(results.meta.fields[i]); - } - results.meta.fields=realFields; - converter.parseMetas=results.meta; - resolve(true); - }, - error:function(error :any) - { - reject(new Error(errors.parserFail)); - }, - download: true, - skipEmptyLines: true, - }); - } - else - reject(new Error(errors.parserNeedUrl)); - }); - } - // Lance FreeDatas2HTML suivant les données reçues : public async run(): Promise { - if(this._datasSourceUrl === "" ) - throw new Error(errors.parserNeedUrl); - - await this.parse(); - - if(this.parseDatas.length === 0 || this.parseMetas!.fields === undefined) // je force avec "!", car l'existence de parseMetas est certaine après parse(). - throw new Error(errors.parserDatasNotFound); - else if(this.stopIfParseErrors && this.parseErrors.length!==0) - console.error(this.parseErrors); + await this.parser.parse(); + if(this.parser.parseResults === undefined) + throw new Error(errors.parserFail); else - { - // Si tout est ok, affichage initial de toutes les données du fichier - this.refreshView(); - return true; + { + this.fields=this.parser.parseResults.fields; + this.datas=this.parser.parseResults.datas; + this.parseErrors=this.parser.parseResults.errors; + if(this.fields === undefined) + throw new Error(errors.parserDatasNotFound); + else if(this.stopIfParseErrors && this.parseErrors !== undefined) + throw new Error(errors.parrserMeetErrors); + else + { + // Si tout est ok, affichage initial de toutes les données du fichier + this.refreshView(); + return true; + } } } refreshView() : void { - if(this.parseMetas === undefined || this.parseMetas.fields === undefined) + if(this.fields === undefined) throw new Error(errors.converterRefreshFail); - this.datasHTML=this.createDatas2Display(this.parseMetas.fields, this.parseDatas); + this.datasHTML=this.createDatas2Display(this.fields, this.datas); if(this._datasViewElt !== undefined && this._datasViewElt.eltDOM !== undefined) this._datasViewElt.eltDOM.innerHTML=this.datasHTML; diff --git a/src/freeDatas2HTMLInterfaces.ts b/src/freeDatas2HTMLInterfaces.ts index d1785a8..1915d4d 100644 --- a/src/freeDatas2HTMLInterfaces.ts +++ b/src/freeDatas2HTMLInterfaces.ts @@ -46,6 +46,36 @@ export interface PaginationsPages values?: number[]; selectedValue?: number; } +export interface Datas +{ + [key: string]: string; +} +export interface ParseErrors // erreurs non bloquantes rencontrées lors du parsage de données +{ + code?: string; + message: string; + row: number; + type?: string; +} +export interface ParseResults +{ + datas: []; + errors: ParseErrors[]; + fields: string[]; +} +export interface Parsers +{ + datasRemoteSource: RemoteSource|undefined; + datas2Parse:string|undefined; + parseResults:ParseResults|undefined; + parse(): Promise; +} +export interface RemoteSource +{ + url: string; + headers?: { key:string, value:string|boolean|number }[] ;// revoir types possibles pour Headers ? + withCredentials?:boolean; +} export interface Selectors { datasViewElt: DOMElement; diff --git a/src/freeDatas2HTMLPagination.ts b/src/freeDatas2HTMLPagination.ts index 2692cda..07abf87 100644 --- a/src/freeDatas2HTMLPagination.ts +++ b/src/freeDatas2HTMLPagination.ts @@ -21,7 +21,7 @@ export class Pagination implements Paginations // De plus l'élément du DOM devant recevoir la liste des pages doit exister constructor(converter: FreeDatas2HTML, pagesElt: DOMElement, pagesName: string="Pages") { - if(converter.parseMetas === undefined || converter.parseMetas.fields === undefined) + if(converter.fields === undefined) throw new Error(errors.paginationNeedDatas); else { diff --git a/src/freeDatas2HTMLParserForCSV.ts b/src/freeDatas2HTMLParserForCSV.ts new file mode 100644 index 0000000..2924f6e --- /dev/null +++ b/src/freeDatas2HTMLParserForCSV.ts @@ -0,0 +1,129 @@ +const Papa = require("papaparse"); +const errors = require("./errors.js"); + +import { ParseResults, Parsers, RemoteSource } from "./freeDatas2HTMLInterfaces"; +interface papaParseOptions +{ + delimiter: string; + newline: string; + quoteChar: string; + escapeChar: string; + transformHeader?(field: string, index: number): string; + preview: number; + comments: false|string, + fastMode: boolean|undefined; + transform?(value: string): string; +} + +export class ParserForCSV implements Parsers +{ + private _datasRemoteSource: RemoteSource|undefined=undefined; + private _datas2Parse:string|undefined=undefined; + private _parseResults:ParseResults|undefined=undefined; + + // Ouverture de certaines options de Papa Parse : + // cf. https://www.papaparse.com/docs#config + public options: papaParseOptions = + { + delimiter: "", + newline: "", + quoteChar: '"', + escapeChar: '"', + transformHeader: function(field: string, index: number): string { return field.trim() }, + preview: 0, + comments: "", + fastMode: undefined, + transform: undefined + } + + set datasRemoteSource(source: RemoteSource) + { + if(source.url.trim().length === 0) + throw new Error(errors.parserNeedUrl); + else + { + source.url=source.url.trim(); + this._datasRemoteSource=source; + } + } + + set datas2Parse(datas: string) + { + if(datas.trim().length === 0) + throw new Error(errors.parserNeedDatas); + else + this._datas2Parse=datas.trim(); + } + + get parseResults() : ParseResults|undefined + { + return this._parseResults; + } + + // async dans le cas d'une source distante + // Et création d'une Promise car PapaParse utilise une fonction callback. + public async parse(): Promise + { + const parser=this, options=this.options; + return new Promise((resolve,reject) => + { + let parseContent="", parseDownload=false, parseDownloadRequestHeaders: any=undefined, parseWithCredentials: any=undefined; + if(parser._datasRemoteSource !== undefined) + { + parseContent=parser._datasRemoteSource.url; + parseDownload=true; + parseWithCredentials=parser._datasRemoteSource.withCredentials; // undefined ok pour PapaParse + if(parser._datasRemoteSource.headers !== undefined) + { + parseDownloadRequestHeaders={}; + for (let i in parser._datasRemoteSource.headers) + parseDownloadRequestHeaders[parser._datasRemoteSource.headers[Number(i)].key]=parser._datasRemoteSource.headers[Number(i)].value; + } + } + else if(parser._datas2Parse !== undefined) + parseContent=parser._datas2Parse; + else + reject(new Error(errors.parserNeedSource)); + + Papa.parse(parseContent, + { + delimiter: options.delimiter, + newline: options.newline, + quoteChar: options.quoteChar, + escapeChar: options.escapeChar, + header: true, // nécessaire pour obtenir le nom des champs + transformHeader: options.transformHeader, + preview: options.preview, + comments: options.comments, + complete: function(results :any) + { + // Attention, Papa Parse peut accepter un nom de champ vide + let realFields: string[]=[]; + for(let i in results.meta.fields) + { + if(results.meta.fields[i].trim() !== "") + realFields.push(results.meta.fields[i]); + } + if(realFields.length === 0) + reject(new Error(errors.parserFail)); + else + { + parser._parseResults={ + datas: results.data, + errors: results.errors, + fields: realFields, + }; + resolve(true); + } + }, + download: parseDownload, + downloadRequestHeaders: parseDownloadRequestHeaders, + skipEmptyLines:"greedy", + fastMode: options.fastMode, + withCredentials: parseWithCredentials, + transform: options.transform, + }); + }); + } + +} \ No newline at end of file diff --git a/src/freeDatas2HTMLRender.ts b/src/freeDatas2HTMLRender.ts index 4bc7996..9fdd225 100644 --- a/src/freeDatas2HTMLRender.ts +++ b/src/freeDatas2HTMLRender.ts @@ -31,7 +31,7 @@ export class Render implements DatasRenders public rend2HTML(datas: any[]) : string { // Il peut n'y avoir aucune donnée (filtres...), mais les noms des champs doivent être connus. - if(this._converter.parseMetas === undefined || this._converter.parseMetas.fields === undefined) + if(this._converter.fields === undefined) throw new Error(errors.renderNeedDatas); else { @@ -40,8 +40,8 @@ export class Render implements DatasRenders if(this.settings.fieldsBegining !== undefined && this.settings.fieldDisplaying !== undefined && this.settings.fieldsEnding !== undefined ) { datasHTML+=this.settings.fieldsBegining; - for (let i in this._converter.parseMetas.fields) - datasHTML+=this.settings.fieldDisplaying.replace("#FIELDNAME", this._converter.parseMetas.fields[Number(i)]); + for (let i in this._converter.fields) + datasHTML+=this.settings.fieldDisplaying.replace("#FIELDNAME", this._converter.fields[Number(i)]); datasHTML+=this.settings.fieldsEnding; } datasHTML+=this.settings.linesBegining; @@ -51,7 +51,7 @@ export class Render implements DatasRenders for(let field in datas[row]) { // On n'affiche que les champs attendus et signale les erreurs dans la console - if(this._converter.parseMetas.fields.indexOf(field) !== -1) + if(this._converter.fields.indexOf(field) !== -1) datasHTML+=this.settings.dataDisplaying.replace("#VALUE" , datas[row][field]).replace("#FIELDNAME" , field); else console.log(errors.renderUnknownField+field); diff --git a/src/freeDatas2HTMLSelector.ts b/src/freeDatas2HTMLSelector.ts index 5f6ca96..6fbe7a4 100644 --- a/src/freeDatas2HTMLSelector.ts +++ b/src/freeDatas2HTMLSelector.ts @@ -15,7 +15,7 @@ export class Selector implements Selectors // Injection de la classe principale, mais uniquement si les données ont été importées constructor(converter: FreeDatas2HTML, datasFieldNb: number, elt: DOMElement) { - if(converter.parseMetas === undefined || converter.parseMetas.fields === undefined || converter.parseDatas.length === 0) + if(converter.fields === undefined || converter.datas.length === 0) throw new Error(errors.selectorNeedDatas); else if(! converter.checkFieldExist(Number(datasFieldNb))) throw new Error(errors.selectorFieldNotFound); @@ -59,18 +59,18 @@ export class Selector implements Selectors throw new Error(errors.selector2HTMLFail); else { - this.name=this._converter.parseMetas!.fields![this._datasFieldNb]; // this._converter.parse... ne peuvent être indéfinis si this._converter existe (cf constructeur) - for (let row in this._converter.parseDatas) + this.name=this._converter.fields![this._datasFieldNb]; // this._converter.parse... ne peuvent être indéfinis si this._converter existe (cf constructeur) + for (let row in this._converter.datas) { if(this._separator === undefined) { - let checkedValue=String(this._converter.parseDatas[row][this.name]).trim(); // trim() nécessaire pour éviter problème de classement du tableau (sort) + let checkedValue=String(this._converter.datas[row][this.name]).trim(); // trim() nécessaire pour éviter problème de classement du tableau (sort) if(checkedValue !== "" && this.values.indexOf(checkedValue) === -1) this.values.push(checkedValue); } else { - let checkedValues=String(this._converter.parseDatas[row][this.name]).split(this._separator); // String() car les données peuvent être des chiffres, etc. + let checkedValues=String(this._converter.datas[row][this.name]).split(this._separator); // String() car les données peuvent être des chiffres, etc. for(let i in checkedValues) { let checkedValue=checkedValues[i].trim(); diff --git a/src/freeDatas2HTMLSortingField.ts b/src/freeDatas2HTMLSortingField.ts index 0031fa8..73dd2bc 100644 --- a/src/freeDatas2HTMLSortingField.ts +++ b/src/freeDatas2HTMLSortingField.ts @@ -13,7 +13,7 @@ export class SortingField implements SortingFields // Injection de la classe principale, mais uniquement si le nom des champs ont été importés et affichés correctement constructor(converter: FreeDatas2HTML, datasFieldNb: number, fieldsDOMSelector: string = "th") { - if(converter.parseMetas === undefined || converter.parseMetas.fields === undefined) + if(converter.fields === undefined) throw new Error(errors.sortingFieldNeedDatas); else if(! converter.checkFieldExist(Number(datasFieldNb))) throw new Error(errors.sortingFieldFieldNotFound); @@ -22,7 +22,7 @@ export class SortingField implements SortingFields const fields=document.querySelectorAll(fieldsDOMSelector); if(fields === undefined) throw new Error(errors.sortingFieldsNotInHTML); - else if(fields.length !== converter.parseMetas.fields.length) + else if(fields.length !== converter.fields.length) throw new Error(errors.sortingFieldsNbFail); else { diff --git a/src/papaParseInterfaces.ts b/src/papaParseInterfaces.ts deleted file mode 100644 index a13c066..0000000 --- a/src/papaParseInterfaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -// cf. https://www.papaparse.com/docs#results - -export interface PapaParseDatas -{ - [key: string]: string; -} - -export interface PapaParseErrors -{ - type: string; - code: string; - message: string; - row: number; -} - -export interface PapaParseMeta -{ - delimiter?: string; - linebreak?: string; - aborted?: boolean; - fields?: string[]; - truncated?: boolean; -} \ No newline at end of file