From 2580677946f47229f1663b4a44e072414fe97018 Mon Sep 17 00:00:00 2001
From: John Konecny <24961694+jfkonecn@users.noreply.github.com>
Date: Mon, 17 Feb 2025 14:30:30 -0500
Subject: [PATCH] Added markdown news support
---
package.json | 3 +
scripts/generate-news-sections.js | 359 ++++++++++++++++++
src/main/webapp/app/config/constants.tsx | 1 +
.../webapp/app/pages/newsPage/NewsList.tsx | 130 ++++---
.../webapp/app/pages/newsPage/NewsPage.tsx | 1 +
.../app/pages/newsPage/NewsPageContent.tsx | 5 +
.../code-generated/NewsContent01302025.tsx | 197 ++++++++++
.../newsPage/markdown/NewsContent01302025.md | 28 ++
yarn.lock | 62 +++
9 files changed, 723 insertions(+), 63 deletions(-)
create mode 100644 scripts/generate-news-sections.js
create mode 100644 src/main/webapp/app/pages/newsPage/code-generated/NewsContent01302025.tsx
create mode 100644 src/main/webapp/app/pages/newsPage/markdown/NewsContent01302025.md
diff --git a/package.json b/package.json
index a1b721fe2..e81e4db4b 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"@types/classnames": "^2.2.9",
"@types/enzyme": "3.10.5",
"@types/jest": "25.2.1",
+ "@types/markdown-it": "^14.1.2",
"@types/node": "13.13.4",
"@types/pluralize": "0.0.29",
"@types/react": "16.9.34",
@@ -143,6 +144,7 @@
"lint-staged": "8.2.1",
"mini-css-extract-plugin": "0.9.0",
"moment-locales-webpack-plugin": "1.2.0",
+ "markdown-it": "^14.1.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "5.0.3",
"postcss-loader": "3.0.0",
@@ -190,6 +192,7 @@
"updateAPI": "yarn run fetchAPI && yarn run buildAPI",
"fetchAPI": "curl http://localhost:9095/v2/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > src/main/webapp/app/shared/api/generated/API-docs.json",
"buildAPI": "node scripts/generate-api.js src/main/webapp/app/shared/api/generated API",
+ "buildNewPages": "node scripts/generate-news-sections.js -f src/main/webapp/app/pages/newsPage/markdown -o src/main/webapp/app/pages/newsPage/code-generated && yarn run prettier --write \"{,src/main/webapp/app/pages/newsPage/code-generated/**/}*.{md,json,ts,tsx,css,scss,yml}\"",
"updateOncoKbAPI": "yarn run fetchOncoKbAPI && yarn run buildOncoKbAPI",
"fetchOncoKbAPI": "curl http://localhost:8080/oncokb-public/api/v1/v2/api-docs?group=Private%20APIs | json | grep -v basePath | grep -v termsOfService | grep -v host > src/main/webapp/app/shared/api/generated/OncoKbAPI-docs.json && curl http://localhost:8080/oncokb-public/api/private/v2/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > src/main/webapp/app/shared/api/generated/OncoKbPrivateAPI-docs.json",
"buildOncoKbAPI": "node scripts/generate-api.js src/main/webapp/app/shared/api/generated OncoKbAPI OncoKbPrivateAPI",
diff --git a/scripts/generate-news-sections.js b/scripts/generate-news-sections.js
new file mode 100644
index 000000000..588a5a9fe
--- /dev/null
+++ b/scripts/generate-news-sections.js
@@ -0,0 +1,359 @@
+const fs = require('fs');
+const path = require('path');
+const MarkdownIt = require('markdown-it');
+
+const oncokbBaseUrls = [
+ 'https://oncokb.org',
+ 'http://oncokb.org',
+ 'https://www.oncokb.org',
+ 'http://www.oncokb.org',
+];
+
+const objectPropertyMark = '-------------------';
+const objectPropertyMarkRegex = new RegExp(
+ `"${objectPropertyMark}(.*)${objectPropertyMark}"`,
+ 'gm'
+);
+
+/**
+ * Escapes special characters in a string to be used in a regular expression.
+ *
+ * This ensures that characters such as `.*+?^${}()|[]\` are treated as literals.
+ *
+ * @param {string} string - The input string to escape.
+ * @returns {string} - The escaped string safe for use in a regex.
+ *
+ * @example
+ * const escaped = escapeRegExp("Hello (world)!");
+ * console.log(escaped); // "Hello \\(world\\)!"
+ */
+function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
+
+/**
+ * Replaces all occurrences of a substring in a given string.
+ *
+ * Since `String.prototype.replaceAll()` is not available in older Node.js versions,
+ * this function uses `RegExp` with the global flag.
+ *
+ * @param {string} str - The original string.
+ * @param {string} match - The substring to be replaced.
+ * @param {string} replacement - The replacement string.
+ * @returns {string} - The modified string with all occurrences replaced.
+ *
+ * @example
+ * const result = replaceAll("Hello world, world!", "world", "Earth");
+ * console.log(result); // "Hello Earth, Earth!"
+ */
+function replaceAll(str, match, replacement) {
+ return str.replace(new RegExp(escapeRegExp(match), 'g'), () => replacement);
+}
+
+/**
+ * Decodes specific HTML entities in a given string.
+ *
+ * This function currently only replaces `"` with `"`.
+ * Extend this function if more entities need to be decoded.
+ *
+ * @param {string} text - The input string containing HTML entities.
+ * @returns {string} - The decoded string.
+ *
+ * @example
+ * const decoded = decodeHtmlEntities("This is "quoted" text.");
+ * console.log(decoded); // 'This is "quoted" text.'
+ */
+function decodeHtmlEntities(text) {
+ return replaceAll(text, '"', '"');
+}
+
+/**
+ * Fixes HTML-escaped entities in a specific pattern within a string.
+ *
+ * This function finds occurrences matching `objectPropertyMarkRegex`,
+ * extracts the inner content, decodes HTML entities, and wraps it in `{}`.
+ *
+ * @param {string} htmlString - The input HTML string to process.
+ * @returns {string} - The fixed string with decoded entities.
+ *
+ * @example
+ * const fixed = fixHtmlString('"----["Example"]----"');
+ * console.log(fixed); // '{["Example]"}'
+ */
+function fixHtmlString(htmlString) {
+ return htmlString.replace(objectPropertyMarkRegex, (_, group) => {
+ return `{${decodeHtmlEntities(group)}}`;
+ });
+}
+
+/**
+ * @param {import('markdown-it')} md - The markdown-it instance used for parsing.
+ * @param {import('markdown-it').StateCore} state - The state object containing Markdown parsing tokens.
+ * @returns {boolean | undefined} - The modified string with all occurrences replaced.
+ *
+ * @throws {Error} If a mutation is found in a row without an associated gene.
+ */
+function newlyAddedGenes(md, state) {
+ let foundNewlyAddedGenes = false;
+ let index = 0;
+ let toRemoveStart = -1;
+ let toRemoveEnd = -1;
+ const genes = [];
+ for (const token of state.tokens) {
+ if (token.content.toLowerCase().includes('new gene')) {
+ foundNewlyAddedGenes = true;
+ toRemoveStart = index;
+ } else if (foundNewlyAddedGenes && token.type === 'bullet_list_close') {
+ toRemoveEnd = index;
+ break;
+ } else if (foundNewlyAddedGenes && token.type === 'inline') {
+ genes.push(token.content);
+ }
+ index++;
+ }
+ if (toRemoveStart < 0 && toRemoveEnd < 0) {
+ return true;
+ } else if (toRemoveStart < 0 || toRemoveEnd < 0) {
+ throw new Error(
+ `Found one remove gene index, but not the other (${toRemoveStart}, ${toRemoveEnd})`
+ );
+ }
+
+ const alterationPageLinkTags = createMarkdownToken(
+ md,
+ 'NewlyAddedGenesListItem'
+ );
+ alterationPageLinkTags[0].attrSet(
+ 'genes',
+ `${objectPropertyMark}${JSON.stringify(genes)}${objectPropertyMark}`
+ );
+ alterationPageLinkTags[1].tag = 'NewlyAddedGenesListItem';
+
+ state.tokens = state.tokens.filter(
+ (_, idx) => idx < toRemoveStart || idx > toRemoveEnd
+ );
+ state.tokens.splice(toRemoveStart, 0, ...alterationPageLinkTags);
+
+ return true;
+}
+
+/**
+ * @param {import('markdown-it').StateCore} state - The state object containing Markdown parsing tokens.
+ * @returns {boolean | undefined} - The modified string with all occurrences replaced.
+ *
+ * @throws {Error} If a mutation is found in a row without an associated gene.
+ */
+function fixLinks(state) {
+ for (const token of state.tokens) {
+ if (token.type === 'inline') {
+ let foundLocalLink = false;
+ for (const child of token.children) {
+ if (child.type === 'link_close' && foundLocalLink) {
+ foundLocalLink = false;
+ child.tag = 'Link';
+ continue;
+ } else if (child.type !== 'link_open') {
+ continue;
+ }
+ const hrefIndex = child.attrIndex('href');
+ if (hrefIndex < 0) {
+ continue;
+ }
+ const currentUrl = child.attrs[hrefIndex][1];
+ let replaceUrlString = '';
+ for (const url of oncokbBaseUrls) {
+ if (currentUrl.startsWith(url)) {
+ replaceUrlString = url;
+ foundLocalLink = true;
+ break;
+ }
+ }
+ if (foundLocalLink) {
+ child.tag = 'Link';
+ const attr = child.attrs[hrefIndex];
+ attr[0] = 'to';
+ attr[1] = currentUrl.replace(replaceUrlString, '');
+ }
+ }
+ }
+ }
+}
+
+/**
+ * @param {import('markdown-it')} md - The markdown-it instance used for token processing.
+ * @param {import('markdown-it').StateCore} state - The state object containing Markdown parsing tokens.
+ * @returns {boolean | undefined} - The modified string with all occurrences replaced.
+ *
+ * @throws {Error} If a mutation is found in a row without an associated gene.
+ */
+function addAutoTableLinks(md, state) {
+ let inTh = false;
+ let inTd = false;
+ let columnIdx = 0;
+ let geneIdx = -1;
+ let mutationIdx = -1;
+ let currentGene = '';
+ for (const token of state.tokens) {
+ if (token.type === 'th_open') {
+ inTh = true;
+ } else if (token.type === 'th_close') {
+ inTh = false;
+ columnIdx++;
+ } else if (token.type === 'td_open') {
+ inTd = true;
+ } else if (token.type === 'td_close') {
+ inTd = false;
+ columnIdx++;
+ } else if (token.type === 'tr_open') {
+ columnIdx = 0;
+ currentGene = '';
+ } else if (inTd && columnIdx === geneIdx && token.type === 'inline') {
+ const child = token.children[0];
+ currentGene = child.content;
+ child.content = `{getAlternativeGenePageLinks('${child.content}')}`;
+ } else if (inTd && columnIdx === mutationIdx && token.type === 'inline') {
+ const child = token.children[0];
+ if (currentGene === '') {
+ throw new Error(`No gene for this row and mutation "${child.content}"`);
+ }
+ const alterationPageLinkTags = createMarkdownToken(
+ md,
+ 'AlterationPageLink'
+ );
+ alterationPageLinkTags[0].attrSet('hugoSymbol', currentGene);
+ alterationPageLinkTags[0].attrSet('alteration', child.content);
+ token.children = alterationPageLinkTags;
+ } else if (inTh && token.content === 'Gene') {
+ geneIdx = columnIdx;
+ } else if (inTh && token.content === 'Mutation') {
+ mutationIdx = columnIdx;
+ }
+ }
+}
+
+/**
+ * Creates a Markdown token with a specified tag.
+ *
+ * @param {import('markdown-it')} md - The markdown-it instance used for parsing.
+ * @param {string} tag - The tag to set on the first non-text token.
+ * @returns {import('markdown-it').Token[]} - An array of markdown-it token objects with the modified tag.
+ *
+ * @example
+ * const md = require('markdown-it')();
+ * const tokens = createMarkdownToken(md, 'custom-tag');
+ * console.log(tokens);
+ */
+function createMarkdownToken(md, tag) {
+ // can't figure out how to make a token on my own so I'm forcing
+ // markdown-it to make me one
+ const alterationPageLinkTags = md
+ .parse('[placeholder](placeholder)', {})[1]
+ .children.filter(x => x.type !== 'text');
+ alterationPageLinkTags[0].type = 'para';
+ alterationPageLinkTags[0].tag = tag;
+ alterationPageLinkTags[0].attrs = [];
+ alterationPageLinkTags[1].tag = tag;
+ return alterationPageLinkTags;
+}
+
+const md = new MarkdownIt({
+ html: true,
+ linkify: true,
+ breaks: true,
+}).use(md => {
+ // https://markdown-it.github.io/markdown-it
+ // https://github.com/markdown-it/markdown-it/blob/master/docs/examples/renderer_rules.md
+ md.renderer.rules.table_open = function () {
+ return '
\n
';
+ };
+
+ md.renderer.rules.table_close = function () {
+ return '
\n
';
+ };
+
+ md.core.ruler.push('add-auto-table-links', state => {
+ addAutoTableLinks(md, state);
+ });
+
+ md.core.ruler.push('fix-links', state => fixLinks(state));
+ md.core.ruler.push('fix-styles', state => {
+ for (const token of state.tokens) {
+ if (token.attrs != null && token.attrs.length > 0) {
+ token.attrs = token.attrs.filter(([name]) => name !== 'style');
+ }
+ if (token.type === 'table_open') {
+ token.attrSet('className', 'table');
+ }
+ }
+ return true;
+ });
+ md.core.ruler.push('newly-added-genes', state => {
+ newlyAddedGenes(md, state);
+ });
+});
+
+const args = process.argv.slice(2);
+let inputFolder = null;
+let outputFolder = null;
+
+for (let i = 0; i < args.length; i++) {
+ if (args[i] === '-f' && args[i + 1]) {
+ inputFolder = path.resolve(args[i + 1]);
+ } else if (args[i] === '-o' && args[i + 1]) {
+ outputFolder = path.resolve(args[i + 1]);
+ }
+}
+
+if (!inputFolder || !outputFolder) {
+ console.error(
+ 'Error: Both -f (input folder) and -o (output folder) arguments are required.'
+ );
+ process.exit(1);
+}
+
+// Ensure input folder exists
+if (!fs.existsSync(inputFolder) || !fs.statSync(inputFolder).isDirectory()) {
+ console.error(
+ `Error: Input folder "${inputFolder}" does not exist or is not a directory.`
+ );
+ process.exit(1);
+}
+
+// Ensure output folder exists, or create it
+if (!fs.existsSync(outputFolder)) {
+ fs.mkdirSync(outputFolder, { recursive: true });
+}
+
+// Read all Markdown files in the input folder
+const files = fs.readdirSync(inputFolder).filter(file => file.endsWith('.md'));
+
+if (files.length === 0) {
+ console.warn(`Warning: No markdown files found in "${inputFolder}".`);
+}
+
+files.forEach(file => {
+ const filePath = path.join(inputFolder, file);
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const htmlContent = fixHtmlString(md.render(content));
+
+ const componentName = path
+ .basename(file, '.md')
+ .replace(/[^a-zA-Z0-9]/g, '_');
+
+ const tsxContent = `import React from 'react';
+import { Link } from 'react-router-dom';
+import { AlterationPageLink, getAlternativeGenePageLinks } from 'app/shared/utils/UrlUtils';
+import { NewlyAddedGenesListItem } from 'app/pages/newsPage/NewlyAddedGenesListItem';
+
+export default function ${componentName}() {
+ return (
+ <>
+ ${htmlContent}
+ >
+ );
+}`;
+
+ const outputFilePath = path.join(outputFolder, `${componentName}.tsx`);
+ fs.writeFileSync(outputFilePath, tsxContent, 'utf-8');
+ console.log(`Generated: ${outputFilePath}`);
+});
diff --git a/src/main/webapp/app/config/constants.tsx b/src/main/webapp/app/config/constants.tsx
index 105bb6bc1..d95031af8 100644
--- a/src/main/webapp/app/config/constants.tsx
+++ b/src/main/webapp/app/config/constants.tsx
@@ -866,6 +866,7 @@ export type DataRelease = {
};
export const DATA_RELEASES: DataRelease[] = [
+ { date: '02302025', version: 'v4.26' },
{ date: '01302025', version: 'v4.25' },
{ date: '12192024', version: 'v4.24' },
{ date: '11262024', version: 'v4.23' },
diff --git a/src/main/webapp/app/pages/newsPage/NewsList.tsx b/src/main/webapp/app/pages/newsPage/NewsList.tsx
index 717d7b50a..2767d2ca0 100644
--- a/src/main/webapp/app/pages/newsPage/NewsList.tsx
+++ b/src/main/webapp/app/pages/newsPage/NewsList.tsx
@@ -19,6 +19,7 @@ import HashLink from 'app/shared/links/HashLink';
import { RouterStore } from 'mobx-react-router';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
+import NewsContent01302025 from './code-generated/NewsContent01302025';
export type NewsListProps = {
date: string;
@@ -100,69 +101,72 @@ export default class NewsList extends React.Component {
{this.props.children ? (
this.props.children
) : newsData ? (
-
- {getNews({
- key: `priority-news-${date}`,
- content: newsData.priorityNews,
- })}
- {newsData.updatedImplication ? (
- {
- return {
- key: `updatedImplication-${date}-${index}`,
- content: item.map((subItem, subIndex) => {
- return {
- key: `updatedImplication-${date}-${index}-${subIndex}`,
- content: subItem,
- };
- }),
- };
- })}
- numOfAssociationsInUpdatedImplication={
- newsData.numOfAssociationsInUpdatedImplication
- }
- />
- ) : undefined}
- {newsData.updatedImplicationInOldFormat ? (
-
- ) : undefined}
- {newsData.changedAnnotations
- ? newsData.changedAnnotations.map(annotation => {
- return (
- {
- return {
- key: `changedAnnotation-${date}-${index}`,
- content: item.map((subItem, subIndex) => {
- return {
- key: `changedAnnotation-${date}-${index}-${subIndex}`,
- content: subItem,
- };
- }),
- };
- })
- : []
- }
- />
- );
- })
- : undefined}
- {getNews({
- key: `news-${date}`,
- content: newsData.news ? newsData.news : [],
- })}
- {this.getNewlyAddGeneSection(newsData)}
-
+ <>
+ {newsData?.markdown}
+
+ {getNews({
+ key: `priority-news-${date}`,
+ content: newsData.priorityNews,
+ })}
+ {newsData.updatedImplication ? (
+ {
+ return {
+ key: `updatedImplication-${date}-${index}`,
+ content: item.map((subItem, subIndex) => {
+ return {
+ key: `updatedImplication-${date}-${index}-${subIndex}`,
+ content: subItem,
+ };
+ }),
+ };
+ })}
+ numOfAssociationsInUpdatedImplication={
+ newsData.numOfAssociationsInUpdatedImplication
+ }
+ />
+ ) : undefined}
+ {newsData.updatedImplicationInOldFormat ? (
+
+ ) : undefined}
+ {newsData.changedAnnotations
+ ? newsData.changedAnnotations.map(annotation => {
+ return (
+ {
+ return {
+ key: `changedAnnotation-${date}-${index}`,
+ content: item.map((subItem, subIndex) => {
+ return {
+ key: `changedAnnotation-${date}-${index}-${subIndex}`,
+ content: subItem,
+ };
+ }),
+ };
+ })
+ : []
+ }
+ />
+ );
+ })
+ : undefined}
+ {getNews({
+ key: `news-${date}`,
+ content: newsData.news ? newsData.news : [],
+ })}
+ {this.getNewlyAddGeneSection(newsData)}
+
+ >
) : undefined}
>
diff --git a/src/main/webapp/app/pages/newsPage/NewsPage.tsx b/src/main/webapp/app/pages/newsPage/NewsPage.tsx
index 9f7899756..cd7fced2a 100644
--- a/src/main/webapp/app/pages/newsPage/NewsPage.tsx
+++ b/src/main/webapp/app/pages/newsPage/NewsPage.tsx
@@ -86,6 +86,7 @@ export default class NewsPage extends React.Component<{
+
diff --git a/src/main/webapp/app/pages/newsPage/NewsPageContent.tsx b/src/main/webapp/app/pages/newsPage/NewsPageContent.tsx
index b2afa656d..0d3bfa5ed 100644
--- a/src/main/webapp/app/pages/newsPage/NewsPageContent.tsx
+++ b/src/main/webapp/app/pages/newsPage/NewsPageContent.tsx
@@ -45,6 +45,7 @@ import { PMALink } from 'app/shared/links/PMALink';
import OptimizedImage from 'app/shared/image/OptimizedImage';
import { AnnotationColumnHeaderType } from './ChangedAnnotationListItem';
import { linkableMutationName, convertGeneInputToLinks } from './Util';
+import NewsContent01302025 from './code-generated/NewsContent01302025';
export type ChangedAnnotation = {
content: (ElementType | ElementType[])[][];
@@ -55,6 +56,7 @@ export type ChangedAnnotation = {
};
export type NewsData = {
+ markdown?: JSX.Element;
priorityNews?: ElementType[];
news?: ElementType[];
newlyAddedGenes?: string[];
@@ -301,6 +303,9 @@ const EVIDENCE_COLUMN_SEPARATOR = '; ';
// https://stackoverflow.com/questions/41947168/is-it-possible-to-use-keyof-operator-on-literals-instead-of-interfaces
export const NEWS_BY_DATE: { [date: string]: NewsData } = {
+ '02282025': {
+ markdown:
,
+ },
'01302025': {
priorityNews: [
diff --git a/src/main/webapp/app/pages/newsPage/code-generated/NewsContent01302025.tsx b/src/main/webapp/app/pages/newsPage/code-generated/NewsContent01302025.tsx
new file mode 100644
index 000000000..adc9a33f3
--- /dev/null
+++ b/src/main/webapp/app/pages/newsPage/code-generated/NewsContent01302025.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import {
+ AlterationPageLink,
+ getAlternativeGenePageLinks,
+} from 'app/shared/utils/UrlUtils';
+import { NewlyAddedGenesListItem } from 'app/pages/newsPage/NewlyAddedGenesListItem';
+
+export default function NewsContent01302025() {
+ return (
+ <>
+
+
+ Happy New Year! As of January X, 2025, fifteen Level 1, twelve Level
+ 2, eighteen Level 3 and eleven Level 4 treatments for unique
+ biomarker-selected indications were added to OncoKB. A table
+ summarizing these changes can be found{' '}
+ here. The “Precision
+ Oncology: 2024 in Review” article can be found{' '}
+
+ here
+
+ .
+
+
+
+ Updated Therapeutic Implications
+
+
+ New alteration(s) with a tumor type-specific level of evidence
+
+
+
+
+
+ Level
+ Gene
+ Mutation
+ Cancer Type
+ Drug(s)
+ Evidence
+
+
+
+
+ 2
+ {getAlternativeGenePageLinks('ERBB2')}
+
+
+
+ Cervical Cancer
+ Neratinib
+
+ Inclusion in Cervical Cancer NCCN Guidelines V1.2025; PMID:{' '}
+ 38211393
+
+
+
+
+
+
+
+
+ Promotion of tumor type-specific level of evidence for an alteration
+
+
+
+
+
+
+ Gene
+ Mutation
+ Cancer Type
+ Drug(s)
+ Previous Level
+ Updated Level
+ Evidence
+
+
+
+
+ {getAlternativeGenePageLinks('KRAS')}
+
+
+
+ Small Bowel Cancer
+
+ Drug(s) promoted in OncoKB™: Adagrasib (Level 2,
+ previously Level 3A) Drug(s) added to OncoKB™: {' '}
+ Sotorasib (Level 2)
+
+ 3A
+ 2
+
+ Inclusion in Small Bowel Adenocarcinoma NCCN Guidelines V1.2025;
+ PMID:{' '}
+ 32955176
+ ,{' '}
+ 37099736
+ ,{' '}
+ 36546659
+ ,{' '}
+ 34919824
+
+
+
+
+
+
+
+ Addition of drug(s) associated with a tumor type-specific leveled
+ alteration(s) currently in OncoKB™ (without changing the alteration's
+ highest level of evidence)
+
+
+
+
+
+
+ Level
+ Gene
+ Mutation
+ Cancer Type
+ Level-Associated Drug(s) in OncoKB™
+ Drug(s) Updated in OncoKBTM
+ Evidence
+
+
+
+
+ 1
+ {getAlternativeGenePageLinks('ALK')}
+
+
+
+ Non-Small Cell Lung Cancer
+
+ Alectinib, Brigatinib, Certinib, Crizotinib, Lorlatinib (Level
+ 1) NVL-655 (Level 3A)
+
+ Ensartinib (Level 1)
+
+
+ FDA approval of ensartinib
+
+ ; PMID:{' '}
+ 34473194
+
+
+
+ 1
+ {getAlternativeGenePageLinks('KRAS')}
+
+
+
+ Colorectal Cancer
+
+ Sensitivity Adagrasib + Cetuximab (Level 1) Adagrasib +
+ Panitumumab, Sotorasib + Cetuximab, Sotorasib + Panitumumab
+ (Level 2) Binimetinib, Cobimetinib, Trametinib (Level 4)
+ Resistance Cetuximab, Panitumumab, Tucatinib + Trastuzumab
+ (Level R1)
+
+ Sotorasib + Panitumumab (Level 1)
+
+
+ FDA approval of sotorasib with panitumumab
+
+ ; PMID:{' '}
+ 37870968
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/main/webapp/app/pages/newsPage/markdown/NewsContent01302025.md b/src/main/webapp/app/pages/newsPage/markdown/NewsContent01302025.md
new file mode 100644
index 000000000..e53b998ea
--- /dev/null
+++ b/src/main/webapp/app/pages/newsPage/markdown/NewsContent01302025.md
@@ -0,0 +1,28 @@
+- Happy New Year\! As of January X, 2025, fifteen Level 1, twelve Level 2, eighteen Level 3 and eleven Level 4 treatments for unique biomarker-selected indications were added to OncoKB. A table summarizing these changes can be found [here](https://www.oncokb.org/year-end-summary#2024). The “Precision Oncology: 2024 in Review” article can be found [here](https://aacrjournals.org/cancerdiscovery/article/14/12/2332/750142/Precision-Oncology-2024-in-ReviewPrecision).
+
+**Updated Therapeutic Implications**
+
+- New alteration(s) with a tumor type-specific level of evidence
+
+| Level | Gene | Mutation | Cancer Type | Drug(s) | Evidence |
+| :---- | :---- | :------------------ | :-------------- | :-------- | :---------------------------------------------------------------------------------------------------------------- |
+| 2 | ERBB2 | Oncogenic Mutations | Cervical Cancer | Neratinib | Inclusion in Cervical Cancer NCCN Guidelines V1.2025; PMID: [38211393](https://pubmed.ncbi.nlm.nih.gov/38211393/) |
+
+####
+
+- Promotion of tumor type-specific level of evidence for an alteration
+
+| Gene | Mutation | Cancer Type | Drug(s) | Previous Level | Updated Level | Evidence |
+| :--- | :------- | :----------------- | :----------------------------------------------------------------------------------------------------------------------- | :------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| KRAS | G12C | Small Bowel Cancer | _Drug(s) promoted in OncoKB™:_ Adagrasib (Level 2, previously Level 3A) _Drug(s) added to OncoKB™:_ Sotorasib (Level 2\) | 3A | 2 | Inclusion in Small Bowel Adenocarcinoma NCCN Guidelines V1.2025; PMID: [32955176](https://pubmed.ncbi.nlm.nih.gov/37099736/), [37099736](https://pubmed.ncbi.nlm.nih.gov/32955176/), [36546659](https://pubmed.ncbi.nlm.nih.gov/36546659/), [34919824](https://pubmed.ncbi.nlm.nih.gov/34919824/) |
+
+- Addition of drug(s) associated with a tumor type-specific leveled alteration(s) currently in OncoKB™ (without changing the alteration's highest level of evidence)
+
+| Level | Gene | Mutation | Cancer Type | Level-Associated Drug(s) in OncoKB™ | Drug(s) Updated in OncoKBTM | Evidence |
+| :---- | :--- | :------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 1 | ALK | Fusions | Non-Small Cell Lung Cancer | Alectinib, Brigatinib, Certinib, Crizotinib, Lorlatinib (Level 1\) NVL-655 (Level 3A) | Ensartinib (Level 1\) | [FDA approval of ensartinib](https://www.fda.gov/drugs/resources-information-approved-drugs/fda-approves-ensartinib-alk-positive-locally-advanced-or-metastatic-non-small-cell-lung-cancer); PMID: [34473194](https://pubmed.ncbi.nlm.nih.gov/34473194/) |
+| 1 | KRAS | G12C | Colorectal Cancer | Sensitivity Adagrasib \+ Cetuximab (Level 1\) Adagrasib \+ Panitumumab, Sotorasib \+ Cetuximab, Sotorasib \+ Panitumumab (Level 2\) Binimetinib, Cobimetinib, Trametinib (Level 4\) Resistance Cetuximab, Panitumumab, Tucatinib \+ Trastuzumab (Level R1) | Sotorasib \+ Panitumumab (Level 1\) | [FDA approval of sotorasib with panitumumab](https://www.fda.gov/drugs/resources-information-approved-drugs/fda-approves-sotorasib-panitumumab-kras-g12c-mutated-colorectal-cancer); PMID: [37870968](https://pubmed.ncbi.nlm.nih.gov/37870968/) |
+
+- Addition of new genes:
+ - AKT1S1
+ - ALOX15B
diff --git a/yarn.lock b/yarn.lock
index f9d82c1c7..8aff57461 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2515,6 +2515,24 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
+"@types/linkify-it@^5":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
+ integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
+
+"@types/markdown-it@^14.1.2":
+ version "14.1.2"
+ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
+ integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
+ dependencies:
+ "@types/linkify-it" "^5"
+ "@types/mdurl" "^2"
+
+"@types/mdurl@^2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
+ integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
+
"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -3398,6 +3416,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
arr-diff@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
@@ -7409,6 +7432,11 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+entities@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
env-paths@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
@@ -11490,6 +11518,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+linkify-it@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
+ integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
+ dependencies:
+ uc.micro "^2.0.0"
+
lint-staged@8.2.1:
version "8.2.1"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.2.1.tgz#752fcf222d9d28f323a3b80f1e668f3654ff221f"
@@ -12027,6 +12062,18 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
+markdown-it@^14.1.0:
+ version "14.1.0"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
+ integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
+ dependencies:
+ argparse "^2.0.1"
+ entities "^4.4.0"
+ linkify-it "^5.0.0"
+ mdurl "^2.0.0"
+ punycode.js "^2.3.1"
+ uc.micro "^2.1.0"
+
matcher@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/matcher/-/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2"
@@ -12058,6 +12105,11 @@ mdn-data@2.0.4:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+mdurl@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
+ integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
+
measure-text@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/measure-text/-/measure-text-0.0.4.tgz#2bfbc3d903b2b1f247d187a29617f807104f0a52"
@@ -14762,6 +14814,11 @@ pumpify@^1.3.3:
inherits "^2.0.3"
pump "^2.0.0"
+punycode.js@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
+ integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
+
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
@@ -18236,6 +18293,11 @@ ua-parser-js@0.7.17:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
integrity sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==
+uc.micro@^2.0.0, uc.micro@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
+ integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
+
uglify-js@3.4.x:
version "3.4.10"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"