diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7b8e9df..ca138c267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ title: Changelog --- +## version 2.15.0 - 2020/05/06 + + ## version 2.14.0 - 2020/04/24 diff --git a/bundlesize.config.json b/bundlesize.config.json index ae91d1d81..74da00b09 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -2,11 +2,11 @@ "files": [ { "path": "./dist/auspice.bundle.js", - "maxSize": "180 kB" + "maxSize": "200 kB" }, { "path": "./dist/auspice.chunk.*.bundle.js", - "maxSize": "110 kB" + "maxSize": "120 kB" } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 18622d9f3..2a507e16b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "auspice", - "version": "2.13.0", + "version": "2.14.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d4864c2c3..f0ba54101 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auspice", - "version": "2.14.0", + "version": "2.15.0", "description": "Web app for visualizing pathogen evolution", "author": "James Hadfield, Trevor Bedford and Richard Neher", "license": "AGPL-3.0-only", diff --git a/src/components/controls/language.js b/src/components/controls/language.js index cc9c83e47..5924155f8 100644 --- a/src/components/controls/language.js +++ b/src/components/controls/language.js @@ -31,7 +31,8 @@ class Language extends React.Component { {value: "pt", label: "Português"}, {value: "fr", label: "Français"}, {value: "tr", label: "Türkçe"}, - {value: "ja", label: "日本語"} + {value: "ja", label: "日本語"}, + {value: "ar", label: "العربية"} ]; return languages; } diff --git a/src/components/framework/footer.js b/src/components/framework/footer.js index d2f29aaac..26a5cd2ae 100644 --- a/src/components/framework/footer.js +++ b/src/components/framework/footer.js @@ -1,7 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import marked from "marked"; -import dompurify from "dompurify"; import styled from 'styled-components'; import { withTranslation } from "react-i18next"; import { FaDownload } from "react-icons/fa"; @@ -13,6 +11,7 @@ import { version } from "../../version"; import { publications } from "../download/downloadModal"; import { isValueValid } from "../../util/globals"; import hardCodedFooters from "./footer-descriptions"; +import { parseMarkdown } from "../../util/parseMarkdown"; const dot = ( @@ -138,36 +137,9 @@ export const getAcknowledgments = (metadata, dispatch) => { * Jover. December 2019. */ if (metadata.description) { - dompurify.addHook("afterSanitizeAttributes", (node) => { - // Set external links to open in a new tab - if ('href' in node && location.hostname !== node.hostname) { - node.setAttribute('target', '_blank'); - node.setAttribute('rel', 'noreferrer nofollow'); - } - // Find nodes that contain images and add imageContainer class to update styling - const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; - if (nodeContainsImg) { - // For special case of image links, set imageContainer on outer parent - if (node.localName === 'a') { - node.parentNode.className += ' imageContainer'; - } else { - node.className += ' imageContainer'; - } - } - }); - - const sanitizer = dompurify.sanitize; - const sanitizerConfig = { - ALLOWED_TAGS: ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img', '#text', 'code', 'pre', 'hr'], - ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'alt'], - KEEP_CONTENT: false, - ALLOW_DATA_ATTR: false - }; - let cleanDescription; try { - const rawDescription = marked(metadata.description); - cleanDescription = sanitizer(rawDescription, sanitizerConfig); + cleanDescription = parseMarkdown(metadata.description); } catch (error) { console.error(`Error parsing footer description: ${error}`); cleanDescription = '

There was an error parsing the footer description.

'; diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index 397d5d9db..9d2994788 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -5,9 +5,12 @@ import { axisBottom, axisLeft } from "d3-axis"; import { rgb } from "d3-color"; import { area } from "d3-shape"; import { format } from "d3-format"; +import _range from "lodash/range"; import { dataFont } from "../../globalStyles"; import { unassigned_label } from "../../util/processFrequencies"; import { isColorByGenotype, decodeColorByGenotype } from "../../util/getGenotype"; +import { numericToCalendar } from "../../util/dateHelpers"; +import { createDisplayDate, calculateMajorGridSeperationForTime } from "../tree/phyloTree/grid"; /* C O N S T A N T S */ const opacity = 0.85; @@ -52,11 +55,11 @@ const getOrderedCategories = (matrixCategories, colorScale) => { return orderedCategories; }; -export const calcXScale = (chartGeom, pivots, ticks) => { +export const calcXScale = (chartGeom, pivots) => { const x = scaleLinear() .domain([pivots[0], pivots[pivots.length - 1]]) .range([chartGeom.spaceLeft, chartGeom.width - chartGeom.spaceRight]); - return {x, numTicksX: ticks.length}; + return {x}; }; export const calcYScale = (chartGeom, maxY) => { @@ -80,13 +83,22 @@ const removeProjectionInfo = (svg) => { }; export const drawXAxis = (svg, chartGeom, scales) => { + const domain = scales.x.domain(), + range = scales.x.range(); + const {majorStep} = calculateMajorGridSeperationForTime( + domain[1] - domain[0], + range[1] - range[0] + ); + const customDate = (date) => createDisplayDate(majorStep, date); removeXAxis(svg); svg.append("g") .attr("class", "x axis") .attr("transform", `translate(0,${chartGeom.height - chartGeom.spaceBottom})`) .style("font-family", dataFont) .style("font-size", "12px") - .call(axisBottom(scales.x).ticks(scales.numTicksX, ".1f")); + .call(axisBottom(scales.x) + .tickValues(_range(domain[0], domain[1], majorStep)) + .tickFormat(customDate)); }; export const drawYAxis = (svg, chartGeom, scales) => { @@ -296,7 +308,7 @@ export const drawStream = ( .style("font-weight", 300) .html( `

${parseColorBy(colorBy, colorOptions)}: ${labels[i]}

-

${t("Time point")}: ${pivots[pivotIdx]}

+

${t("Time point")}: ${numericToCalendar(pivots[pivotIdx])}

${frequencyText}: ${freqVal}

` ); } diff --git a/src/components/frequencies/index.js b/src/components/frequencies/index.js index 703932add..170e972d2 100644 --- a/src/components/frequencies/index.js +++ b/src/components/frequencies/index.js @@ -12,7 +12,6 @@ import "../../css/entropy.css"; return { data: state.frequencies.data, pivots: state.frequencies.pivots, - ticks: state.frequencies.ticks, matrix: state.frequencies.matrix, projection_pivot: state.frequencies.projection_pivot, version: state.frequencies.version, @@ -36,7 +35,7 @@ class Frequencies extends React.Component { const data = processMatrix({...props}); newState.maxY = data.maxY; newState.categories = data.categories; - const scalesX = calcXScale(chartGeom, props.pivots, props.ticks); + const scalesX = calcXScale(chartGeom, props.pivots); const scalesY = calcYScale(chartGeom, data.maxY); newState.scales = {...scalesX, ...scalesY}; drawXAxis(newState.svg, chartGeom, scalesX); diff --git a/src/components/narrative/MainDisplayMarkdown.js b/src/components/narrative/MainDisplayMarkdown.js index 49a89e3ef..15750ace6 100644 --- a/src/components/narrative/MainDisplayMarkdown.js +++ b/src/components/narrative/MainDisplayMarkdown.js @@ -1,10 +1,8 @@ import React from "react"; import { connect } from "react-redux"; -import marked from "marked"; import styled from 'styled-components'; -import dompurify from "dompurify"; import { dataFont } from "../../globalStyles"; - +import { parseMarkdown } from "../../util/parseMarkdown"; /** * The following code borrows heavily from the Footer @@ -105,7 +103,7 @@ const Container = styled.div` `; const EXPERIMENTAL_MainDisplayMarkdown = ({narrativeBlock, width, mobile}) => { - const cleanHTML = mdToHtml(narrativeBlock.mainDisplayMarkdown); + const cleanHTML = parseMarkdown(narrativeBlock.mainDisplayMarkdown); return (
{ export default connect((state) => ({ narrativeBlock: state.narrative.blocks[state.narrative.blockIdx] }))(EXPERIMENTAL_MainDisplayMarkdown); - -function mdToHtml(md) { - /* this is copy & pasted from `../framework/footer.js` and should be abstracted - into a function */ - dompurify.addHook("afterSanitizeAttributes", (node) => { - // Set external links to open in a new tab - if ('href' in node && location.hostname !== node.hostname) { - node.setAttribute('target', '_blank'); - node.setAttribute('rel', 'noreferrer nofollow'); - } - // Find nodes that contain images and add imageContainer class to update styling - const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; - if (nodeContainsImg) { - // For special case of image links, set imageContainer on outer parent - if (node.localName === 'a') { - node.parentNode.className += ' imageContainer'; - } else { - node.className += ' imageContainer'; - } - } - }); - - const sanitizer = dompurify.sanitize; - const sanitizerConfig = { - ALLOWED_TAGS: ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img', '#text', 'pre', 'hr', 'table', 'thead', 'tbody', 'th', 'tr', 'td'], - ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'alt'], - KEEP_CONTENT: false, - ALLOW_DATA_ATTR: false - }; - const rawDescription = marked(md); - const cleanDescription = sanitizer(rawDescription, sanitizerConfig); - return cleanDescription; -} diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js index 049403bd7..3caf435de 100644 --- a/src/components/tree/phyloTree/grid.js +++ b/src/components/tree/phyloTree/grid.js @@ -49,79 +49,80 @@ const addSVGGroupsIfNeeded = (groups, svg) => { * Create the major-grid-line separation for divergence scales. * @param {numeric} range num years or amount of divergence present in current view * @param {numeric} minorTicks num of minor ticks desired between each major step - * @returns {array} [0] {numeric} space between major x-axis gridlines (measure of divergence) - * [1] {numeric} space between minor x-axis gridlines (measure of divergence) + * @returns {object} + * - property `majorStep` {numeric}: space between major x-axis gridlines (measure of divergence) + * - property `minorStep` {numeric}: space between minor x-axis gridlines (measure of divergence) */ const calculateMajorGridSeperationForDivergence = (range, minorTicks) => { /* make an informed guess of the step size to start with. E.g. 0.07 => step of 0.01, 70 => step size of 10 */ const logRange = Math.floor(Math.log10(range)); - let step = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties + let majorStep = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties - if (range/step < 2) { // if step > 0.5*range then make more fine-grained steps - step /= 5; - } else if (range/step <5) { // if step > 0.2*range then make more fine grained steps - step /= 2; + if (range/majorStep < 2) { // if step > 0.5*range then make more fine-grained steps + majorStep /= 5; + } else if (range/majorStep <5) { // if step > 0.2*range then make more fine grained steps + majorStep /= 2; } let numMinorTicks = minorTicks; - if (step===5 || step===10) { + if (majorStep===5 || majorStep===10) { numMinorTicks = 5; } - const minorStep = step / numMinorTicks; + const minorStep = majorStep / numMinorTicks; - return [step, minorStep]; + return {majorStep, minorStep}; }; /** * Create the major-grid-line separation for temporal view. * @param {numeric} timeRange num years in current view * @param {numeric} pxAvailable number of pixels available for the x axis - * @returns {array} [0] {numeric} space between major x-axis gridlines (measure of time) - * [1] {numeric} space between minor x-axis gridlines (measure of time) + * @returns {object} + * - property `majorStep` {numeric}: space between major x-axis gridlines (measure of time) + * - property `minorStep` {numeric}: space between minor x-axis gridlines (measure of time) */ -const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => { - +export const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => { const rountToNearest = (n, p) => Math.ceil(n/p)*p; - const getMinorSpacing = (majorTimeStep) => { + const getMinorSpacing = (majorStep) => { const timesToTry = [1/365.25, 1/52, 1/12, 1, 10, 100, 1000]; for (const t of timesToTry) { - const n = majorTimeStep / t; + const n = majorStep / t; // max number we allow is 12 (so that a major grid of a year can have minor grids of a month) if (n <= 12) return t; } - return majorTimeStep; // fallthrough. Only happens for _very_ large trees + return majorStep; // fallthrough. Only happens for _very_ large trees }; /* in general, we find that 1 major point for every ~100px works well for wider displays we shift up to 150px then 200px */ const nSteps = Math.floor(pxAvailable / (pxAvailable < 1200 ? 100 : 150)) || 1; - let majorTimeStep = timeRange / nSteps; + let majorStep = timeRange / nSteps; /* For time views, it's nicer if the spacing is meaningful. There's probably a better way to do this than cascading through levels */ - if (majorTimeStep > 100) { - majorTimeStep = rountToNearest(majorTimeStep, 100); - } else if (majorTimeStep > 10) { - majorTimeStep = rountToNearest(majorTimeStep, 10); - } else if (majorTimeStep > 1) { - majorTimeStep = rountToNearest(majorTimeStep, 1); - } else if (majorTimeStep > (1/12)) { + if (majorStep > 100) { + majorStep = rountToNearest(majorStep, 100); + } else if (majorStep > 10) { + majorStep = rountToNearest(majorStep, 10); + } else if (majorStep > 1) { + majorStep = rountToNearest(majorStep, 1); + } else if (majorStep > (1/12)) { /* each step is longer than a month, but shorter than a year */ - majorTimeStep = rountToNearest(majorTimeStep, 1/12); - } else if (majorTimeStep > (1/52)) { + majorStep = rountToNearest(majorStep, 1/12); + } else if (majorStep > (1/52)) { /* each step is longer than a week, but shorter than a month */ - majorTimeStep = rountToNearest(majorTimeStep, 1/52); - } else if (majorTimeStep > (1/365.25)) { + majorStep = rountToNearest(majorStep, 1/52); + } else if (majorStep > (1/365.25)) { /* each time step is longer than a day, but shorter than a week */ - majorTimeStep = rountToNearest(majorTimeStep, 1/365.25); + majorStep = rountToNearest(majorStep, 1/365.25); } else { - majorTimeStep = 1/365.25; + majorStep = 1/365.25; } - const minorTimeStep = getMinorSpacing(majorTimeStep); - return [majorTimeStep, minorTimeStep]; + const minorStep = getMinorSpacing(majorStep); + return {majorStep, minorStep}; }; /** @@ -130,7 +131,7 @@ const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => { * @param {numeric} numDate date in decimal format * @returns {string} date to be displayed below major gridline */ -const createDisplayDate = (step, numDate) => { +export const createDisplayDate = (step, numDate) => { if (step >= 1) { return numDate.toFixed(Math.max(0, -Math.floor(Math.log10(step)))); } @@ -147,24 +148,24 @@ const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxA const minorGridPoints = []; /* step is the amount (same units of xmax, xmin) of seperation between major grid lines */ - const [step, minorStep] = distanceMeasure === "num_date" ? + const {majorStep, minorStep} = distanceMeasure === "num_date" ? calculateMajorGridSeperationForTime(xmax-xmin, Math.abs(pxAvailable)) : calculateMajorGridSeperationForDivergence(xmax-xmin, minorTicks); - const gridMin = Math.floor(xmin/step)*step; + const gridMin = Math.floor(xmin/majorStep)*majorStep; const minVis = layout==="radial" ? xmin : gridMin; const maxVis = xmax; - for (let ii = 0; ii <= (xmax - gridMin)/step+3; ii++) { - const pos = gridMin + step*ii; + for (let ii = 0; ii <= (xmax - gridMin)/majorStep+3; ii++) { + const pos = gridMin + majorStep*ii; majorGridPoints.push({ position: pos, name: distanceMeasure === "num_date" ? - createDisplayDate(step, pos) : - pos.toFixed(Math.max(0, -Math.floor(Math.log10(step)))), + createDisplayDate(majorStep, pos) : + pos.toFixed(Math.max(0, -Math.floor(Math.log10(majorStep)))), visibility: ((posmaxVis)) ? "hidden" : "visible", axis: "x" }); - for (let minorPos=pos+minorStep; minorPos<(pos+step) && minorPosmaxVis+minorStep)) ? "hidden" : "visible", @@ -179,7 +180,7 @@ const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxA const computeYGridPoints = (ymin, ymax) => { const majorGridPoints = []; let yStep = 0; - yStep = calculateMajorGridSeperationForDivergence(ymax-ymin)[0]; + yStep = calculateMajorGridSeperationForDivergence(ymax-ymin).majorStep; const precisionY = Math.max(0, -Math.floor(Math.log10(yStep))); const gridYMin = Math.floor(ymin/yStep)*yStep; const maxYVis = ymax; diff --git a/src/css/global.css b/src/css/global.css index 77df9e8a4..d7eb5fd77 100644 --- a/src/css/global.css +++ b/src/css/global.css @@ -2,7 +2,6 @@ html, p, div, input, button { font-family: "Lato", "Helvetica Neue", "Helvetica", "sans-serif"; font-weight: 400; - color: "#333"; } /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ diff --git a/src/locales/ar/language.json b/src/locales/ar/language.json new file mode 100644 index 000000000..56809e5e5 --- /dev/null +++ b/src/locales/ar/language.json @@ -0,0 +1,3 @@ +{ + "name": "العربية" +} \ No newline at end of file diff --git a/src/locales/ar/sidebar.json b/src/locales/ar/sidebar.json new file mode 100644 index 000000000..003bdaffa --- /dev/null +++ b/src/locales/ar/sidebar.json @@ -0,0 +1,33 @@ +{ + "Dataset": "مجموعة بيانات", + "Date Range": "النطاق الزمني", + "Color By": "اللون حسب", + "Tree Options": "خيارات الشجرة", + "Layout": "التخطيط", + "rectangular": "مستطيل", + "radial": "شعاعي", + "unrooted": "بدون جذور", + "clock": "زمني", + "Branch Length": "طول الفرع", + "time": "الوقت", + "divergence": "تباعد", + "Show confidence intervals": "اظهار مجال الثقة", + "Branch Labels": "تسميات الفروع", + "Search Strains": "البحث على السلالات", + "Second Tree": "الشجرة الثانية", + "Map Options": "خيارات الخريطة", + "Geographic resolution": "دقة الخريطة", + "Animation Speed": "سرعة الحركة", + "Loop animation": "حركة متكررة", + "Animate cumulative history": "تحريك التاريخ التراكمي", + "Panel Options": "خيارات اللوحة", + "Show tree": "اظهار الشجرة", + "Show map": "اظهار الخريطة", + "Show entropy": "اظهار الانتروبيا", + "Language": "اللغة", + "Slow": "بطيئة", + "Medium": "متوسطة", + "Fast": "سريعة", + "full": "كامل", + "grid": "شبكة" +} \ No newline at end of file diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json new file mode 100644 index 000000000..626f6496c --- /dev/null +++ b/src/locales/ar/translation.json @@ -0,0 +1,91 @@ +{ + "__Header//Byline__": "########################################", + "Maintained by": "الصيانة من طرف", + "using data from": "باستخدام البيانات من", + "Built with": "أنشأ باستعمال", + + + "__Header//Info__": "########################################", + "Showing {{x}} of {{y}} genomes": "جينوم {{y}} من {{x}} إظهار", + "Showing {{x}} of {{y}} genomes sampled between {{from}} and {{to}}": "{{to}} إلى {{from}} إظهار {{x}} من {{y}} جنوم أخدت من ", + "Comprising": "يتكون من", + "Animation in progress": "حركة جارية", + "Filtered to": "صغر إلى", + + + "__Download Modal__": "########################################", + "click outside this box to return to the app": "انقر خارج هذا المربع للعودة إلى التطبيق", + "last updated": "آخر تحديث", + "A full list of sequence authors is available via the TSV files below": "قائمة كاملة من المؤلفين متاحة عبر ملفات أدناه", + "Data usage policy": "سياسة استخدام البيانات", + "Please cite the authors who contributed genomic data (where relevant), as well as": "يرجى ذكر المؤلفين الذين ساهموا بالببيانات الجينومية، وكذلك", + "Download data": "تحميل البيانات", + "Data usage part 1": "الغرض من البيانات المقدمة هنا هو النشر بسرعة تحليل مسببات الأمراض الخطيرة. يتم تضمين البيانات غير المنشورة بإذن من ممتلكي البيانات ، ولا تؤثر على حقهم في النشر ", + "Data usage part 2": "يرجى الاتصال بالمؤلفين المعنيين (متاح عبر ملفات أدناه) إذا كنت تنوي إجراء المزيد من البحث باستخدام بياناتهم. يمكن تنزيل البيانات المشتقة ، مثل السلالات ، أدناه - يرجى الاتصال بالمؤلفين المعنيين عند الحاجة.", + + "__Entropy Panel__": "########################################", + "Diversity": "التنوع", + "entropy": "الانتروبيا", + "events": "الأحداث", + "Codon {{codon}} in protein {{protein}}": "شيفرة جينية {{codon}} في البروتين {{protein}}", + "Nucleotide {{nuc}}": "نوكليوتيد {{nuc}}", + "Nuc positions {{a}} to {{b}}": "موضع النيكليوتيدات {{a}} إلى {{b}}", + "Num mutations": "طفرات النيكليوتيدات", + "Negative strand": " اتجاه إيجابي ", + "Positive strand": "اتجاه سلبي", + "Click to color tree & map": "انقر لتلوين الشجرة والخريطة", + + + "__Footer__": "########################################", + "Filter by {{filterTitle}}": "تصنيف بواسطة {{filterTitle}}", + "Data updated": "بيانات محدثة", + + + "__Map panel__": "########################################", + "Transmissions": "الإرساليات", + "Geography": "الجغرافيا", + "reset zoom": "إعادة التكبير", + "Reset": "إعادة التعيين", + "Play": "تشغيل", + "Pause": "إيقاف", + + + "__Tree (Phylogeny) panel__": "########################################", + "Phylogeny": "شجرة تطور السلالات", + "Reset Layout": "إعادة تعيين التصميم", + "Click on tip to display more info": "انقر على النصيحة لعرض المزيد من المعلومات", + "Click to zoom into clade": "انقر لتكبير الفرع الحيوي", + "Click to zoom out to parent clade": "انقر لتكبير سلف الفرع الحيوي ", + "Branch leading to": "الفرع المؤدي الى", + "Number of descendants": "النسل", + "Nucleotide mutations": "طفرات النيكليوتيد", + "AA mutations": "AA طفرات", + "protein mutations truncated": "طفرات البروتين مقتطعة", + "Gaps": "ثغرات", + "{{x}} more": "{{x}} إضافية ", + "No nucleotide mutations": "بدون طفرات النيكليوتيد ", + "No amino acid mutations": "بدون طفرات الأحماض الأمينية", + "Divergence": "تباعد", + "Date": "التاريخ", + "Collection date": "تاريخ التجميع", + "Inferred collection date": "تاريخ التجميع المستنتح", + "Inferred Date": "التاريخ المستنتج", + "Date Confidence Interval": "مجال ثقة التاريخ", + "Vaccine selected": "اللقاح المحدد", + "Vaccine start date": "تاريخ بداية اللقاح", + "Vaccine end date": "تاريخ نهاية اللقاح", + "Serum strain": "سلالة المصل", + "Click outside this box to go back to the tree": "انقر خارج هذا المربع للعودة إلى الشجرة", + "Authors": "المؤلفون", + "Title": "العنوان", + "Journal": "صحيفة", + + + "__Frequencies panel__": "########################################", + "Frequencies": "الترددات", + "colored by": "ملونة بواسطة", + "Projection": "إسقاط", + "Time point": "نقطة زمنية", + "Frequency": "تردد", + "Projected frequency": "التردد المتوقع" +} \ No newline at end of file diff --git a/src/locales/fr/sidebar.json b/src/locales/fr/sidebar.json index 9d2ac1ef9..7ed38c1de 100644 --- a/src/locales/fr/sidebar.json +++ b/src/locales/fr/sidebar.json @@ -1,5 +1,5 @@ { - "Dataset": "Catalogue des données", + "Dataset": "Jeu de données", "Date Range": "Intervalle de dates", "Color By": "Couleur par", "Tree Options": "Options d'arborescence", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 32dc4e156..31c4f4a39 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -78,7 +78,7 @@ "Click outside this box to go back to the tree": "Cliquez en dehors de cette case pour revenir à l'arbre", "Authors": "Auteurs", "Title": "Titre", - "Journal": "", + "Journal": "Journal", "__Frequencies panel__": "########################################", diff --git a/src/reducers/frequencies.js b/src/reducers/frequencies.js index 4f4e7b904..0eab9c033 100644 --- a/src/reducers/frequencies.js +++ b/src/reducers/frequencies.js @@ -5,7 +5,6 @@ const frequencies = (state = { loaded: false, data: undefined, pivots: undefined, - ticks: undefined, matrix: undefined, projection_pivot: undefined, version: 0 @@ -18,7 +17,7 @@ const frequencies = (state = { return Object.assign({}, state, {loaded: true, matrix: action.matrix, version: state.version + 1}); } case types.DATA_INVALID: { - return {loaded: false, data: undefined, pivots: undefined, ticks: undefined, matrix: undefined, projection_pivot: undefined, version: 0}; + return {loaded: false, data: undefined, pivots: undefined, matrix: undefined, projection_pivot: undefined, version: 0}; } default: return state; diff --git a/src/util/parseMarkdown.js b/src/util/parseMarkdown.js new file mode 100644 index 000000000..d09ef792e --- /dev/null +++ b/src/util/parseMarkdown.js @@ -0,0 +1,69 @@ +import marked from "marked"; +import dompurify from "dompurify"; + +dompurify.addHook("afterSanitizeAttributes", (node) => { + // Set external links to open in a new tab + if ('href' in node && location.hostname !== node.hostname) { + node.setAttribute('target', '_blank'); + node.setAttribute('rel', 'noreferrer nofollow'); + } + // Find nodes that contain images and add imageContainer class to update styling + const nodeContainsImg = ([...node.childNodes].filter((child) => child.localName === 'img')).length > 0; + if (nodeContainsImg) { + // For special case of image links, set imageContainer on outer parent + if (node.localName === 'a') { + node.parentNode.className += ' imageContainer'; + } else { + node.className += ' imageContainer'; + } + } +}); + +const ALLOWED_TAGS = ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img']; +ALLOWED_TAGS.push('#text', 'code', 'pre', 'hr', 'table', 'thead', 'tbody', 'th', 'tr', 'td'); +// We want to support SVG elements, requiring the following tags (we exclude "foreignObject", "style" and "script") +ALLOWED_TAGS.push("svg", "altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion", "animateTransform"); +ALLOWED_TAGS.push("circle", "clipPath", "color-profile", "cursor", "defs", "desc", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer"); +ALLOWED_TAGS.push("feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feFlood", "feFuncA"); +ALLOWED_TAGS.push("feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset"); +ALLOWED_TAGS.push("fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font", "font-face"); +ALLOWED_TAGS.push("font-face-format", "font-face-name", "font-face-src", "font-face-uri", "g", "glyph", "glyphRef"); +ALLOWED_TAGS.push("hkern", "image", "line", "linearGradient", "marker", "mask", "metadata", "missing-glyph", "mpath", "path"); +ALLOWED_TAGS.push("pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "switch", "symbol"); +ALLOWED_TAGS.push("text", "textPath", "title", "tref", "tspan", "use", "view", "vkern"); + +const ALLOWED_ATTR = ['href', 'src', 'width', 'height', 'alt']; +// We add the following Attributes for SVG via https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute +// Certain values have been excluded here, e.g. "style" +ALLOWED_ATTR.push("accent-height", "accumulate", "additive", "alignment-baseline", "allowReorder", "alphabetic", "amplitude", "arabic-form", "ascent", "attributeName", "attributeType", "autoReverse", "azimuth"); +ALLOWED_ATTR.push("baseFrequency", "baseline-shift", "baseProfile", "bbox", "begin", "bias", "by"); +ALLOWED_ATTR.push("calcMode", "cap-height", "class", "clip", "clipPathUnits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cursor", "cx", "cy"); +ALLOWED_ATTR.push("d", "decelerate", "descent", "diffuseConstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy"); +ALLOWED_ATTR.push("edgeMode", "elevation", "enable-background", "end", "exponent", "externalResourcesRequired"); +ALLOWED_ATTR.push("fill", "fill-opacity", "fill-rule", "filter", "filterRes", "filterUnits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "format", "from", "fr", "fx", "fy"); +ALLOWED_ATTR.push("g1", "g2", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "glyphRef", "gradientTransform", "gradientUnits"); +ALLOWED_ATTR.push("hanging", "height", "href", "hreflang", "horiz-adv-x", "horiz-origin-x"); +ALLOWED_ATTR.push("id", "ideographic", "image-rendering", "in", "in2", "intercept"); +ALLOWED_ATTR.push("k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes"); +ALLOWED_ATTR.push("lang", "lengthAdjust", "letter-spacing", "lighting-color", "limitingConeAngle", "local"); +ALLOWED_ATTR.push("marker-end", "marker-mid", "marker-start", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method", "min", "mode"); +ALLOWED_ATTR.push("name", "numOctaves"); +ALLOWED_ATTR.push("offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overline-position", "overline-thickness"); +ALLOWED_ATTR.push("panose-1", "paint-order", "path", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "ping", "pointer-events", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits"); +ALLOWED_ATTR.push("r", "radius", "referrerPolicy", "refX", "refY", "rel", "rendering-intent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry"); +ALLOWED_ATTR.push("scale", "seed", "shape-rendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "string", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "surfaceScale", "systemLanguage"); +ALLOWED_ATTR.push("tabindex", "tableValues", "target", "targetX", "targetY", "text-anchor", "text-decoration", "text-rendering", "textLength", "to", "transform", "type"); +ALLOWED_ATTR.push("u1", "u2", "underline-position", "underline-thickness", "unicode", "unicode-bidi", "unicode-range", "units-per-em"); +ALLOWED_ATTR.push("v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "values", "vector-effect", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox", "viewTarget", "visibility"); +ALLOWED_ATTR.push("width", "widths", "word-spacing", "writing-mode"); +ALLOWED_ATTR.push("x", "x-height", "x1", "x2", "xChannelSelector"); +ALLOWED_ATTR.push("y", "y1", "y2", "yChannelSelector"); +ALLOWED_ATTR.push("z", "zoomAndPan"); + +export const parseMarkdown = (mdString) => { + const sanitizer = dompurify.sanitize; + const sanitizerConfig = {ALLOWED_TAGS, ALLOWED_ATTR, KEEP_CONTENT: false, ALLOW_DATA_ATTR: false}; + const rawDescription = marked(mdString); + const cleanDescription = sanitizer(rawDescription, sanitizerConfig); + return cleanDescription; +}; diff --git a/src/util/processFrequencies.js b/src/util/processFrequencies.js index 05fcf424c..0cd55057f 100644 --- a/src/util/processFrequencies.js +++ b/src/util/processFrequencies.js @@ -69,11 +69,6 @@ export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorS export const processFrequenciesJSON = (rawJSON, tree, controls) => { /* this function can throw */ const pivots = rawJSON.pivots.map((d) => Math.round(parseFloat(d) * 100) / 100); - const ticks = [pivots[0]]; - const tick_step = (pivots[pivots.length - 1] - pivots[0]) / 6 * 10 / 10; - while (ticks[ticks.length - 1] < pivots[pivots.length - 1]) { - ticks.push((ticks[ticks.length - 1] + tick_step) * 10 / 10); - } let projection_pivot = null; if ("projection_pivot" in rawJSON) { projection_pivot = Math.round(parseFloat(rawJSON.projection_pivot) * 100) / 100; @@ -104,7 +99,6 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => { return { data, pivots, - ticks, matrix, projection_pivot }; diff --git a/src/version.js b/src/version.js index 73542891d..57e6a677e 100644 --- a/src/version.js +++ b/src/version.js @@ -1,4 +1,4 @@ -const version = "2.14.0"; +const version = "2.15.0"; module.exports = { version