Skip to content

Commit 4c1e967

Browse files
jfkonecnoncokb-bot
and
oncokb-bot
authored
v4.26 (#1203)
* Update pom version * Added markdown news support --------- Co-authored-by: oncokb-bot <[email protected]>
1 parent b356f42 commit 4c1e967

16 files changed

+803
-71
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@types/classnames": "^2.2.9",
9090
"@types/enzyme": "3.10.5",
9191
"@types/jest": "25.2.1",
92+
"@types/markdown-it": "^14.1.2",
9293
"@types/node": "13.13.4",
9394
"@types/pluralize": "0.0.29",
9495
"@types/react": "16.9.34",
@@ -143,6 +144,7 @@
143144
"lint-staged": "8.2.1",
144145
"mini-css-extract-plugin": "0.9.0",
145146
"moment-locales-webpack-plugin": "1.2.0",
147+
"markdown-it": "^14.1.0",
146148
"node-sass": "^4.12.0",
147149
"optimize-css-assets-webpack-plugin": "5.0.3",
148150
"postcss-loader": "3.0.0",
@@ -190,6 +192,7 @@
190192
"updateAPI": "yarn run fetchAPI && yarn run buildAPI",
191193
"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",
192194
"buildAPI": "node scripts/generate-api.js src/main/webapp/app/shared/api/generated API",
195+
"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}\"",
193196
"updateOncoKbAPI": "yarn run fetchOncoKbAPI && yarn run buildOncoKbAPI",
194197
"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",
195198
"buildOncoKbAPI": "node scripts/generate-api.js src/main/webapp/app/shared/api/generated OncoKbAPI OncoKbPrivateAPI",
Loading

scripts/generate-news-sections.js

+361
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const MarkdownIt = require('markdown-it');
4+
5+
const oncokbBaseUrls = [
6+
'https://oncokb.org',
7+
'http://oncokb.org',
8+
'https://www.oncokb.org',
9+
'http://www.oncokb.org',
10+
'https://beta.oncokb.org',
11+
'http://beta.oncokb.org',
12+
];
13+
14+
const objectPropertyMark = '-------------------';
15+
const objectPropertyMarkRegex = new RegExp(
16+
`"${objectPropertyMark}(.*)${objectPropertyMark}"`,
17+
'gm'
18+
);
19+
20+
/**
21+
* Escapes special characters in a string to be used in a regular expression.
22+
*
23+
* This ensures that characters such as `.*+?^${}()|[]\` are treated as literals.
24+
*
25+
* @param {string} string - The input string to escape.
26+
* @returns {string} - The escaped string safe for use in a regex.
27+
*
28+
* @example
29+
* const escaped = escapeRegExp("Hello (world)!");
30+
* console.log(escaped); // "Hello \\(world\\)!"
31+
*/
32+
function escapeRegExp(string) {
33+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
34+
}
35+
36+
/**
37+
* Replaces all occurrences of a substring in a given string.
38+
*
39+
* Since `String.prototype.replaceAll()` is not available in older Node.js versions,
40+
* this function uses `RegExp` with the global flag.
41+
*
42+
* @param {string} str - The original string.
43+
* @param {string} match - The substring to be replaced.
44+
* @param {string} replacement - The replacement string.
45+
* @returns {string} - The modified string with all occurrences replaced.
46+
*
47+
* @example
48+
* const result = replaceAll("Hello world, world!", "world", "Earth");
49+
* console.log(result); // "Hello Earth, Earth!"
50+
*/
51+
function replaceAll(str, match, replacement) {
52+
return str.replace(new RegExp(escapeRegExp(match), 'g'), () => replacement);
53+
}
54+
55+
/**
56+
* Decodes specific HTML entities in a given string.
57+
*
58+
* This function currently only replaces `&quot;` with `"`.
59+
* Extend this function if more entities need to be decoded.
60+
*
61+
* @param {string} text - The input string containing HTML entities.
62+
* @returns {string} - The decoded string.
63+
*
64+
* @example
65+
* const decoded = decodeHtmlEntities("This is &quot;quoted&quot; text.");
66+
* console.log(decoded); // 'This is "quoted" text.'
67+
*/
68+
function decodeHtmlEntities(text) {
69+
return replaceAll(text, '&quot;', '"');
70+
}
71+
72+
/**
73+
* Fixes HTML-escaped entities in a specific pattern within a string.
74+
*
75+
* This function finds occurrences matching `objectPropertyMarkRegex`,
76+
* extracts the inner content, decodes HTML entities, and wraps it in `{}`.
77+
*
78+
* @param {string} htmlString - The input HTML string to process.
79+
* @returns {string} - The fixed string with decoded entities.
80+
*
81+
* @example
82+
* const fixed = fixHtmlString('"----[&quot;Example&quot;]----"');
83+
* console.log(fixed); // '{["Example]"}'
84+
*/
85+
function fixHtmlString(htmlString) {
86+
return htmlString.replace(objectPropertyMarkRegex, (_, group) => {
87+
return `{${decodeHtmlEntities(group)}}`;
88+
});
89+
}
90+
91+
/**
92+
* @param {import('markdown-it')} md - The markdown-it instance used for parsing.
93+
* @param {import('markdown-it').StateCore} state - The state object containing Markdown parsing tokens.
94+
* @returns {boolean | undefined} - The modified string with all occurrences replaced.
95+
*
96+
* @throws {Error} If a mutation is found in a row without an associated gene.
97+
*/
98+
function newlyAddedGenes(md, state) {
99+
let foundNewlyAddedGenes = false;
100+
let index = 0;
101+
let toRemoveStart = -1;
102+
let toRemoveEnd = -1;
103+
const genes = [];
104+
for (const token of state.tokens) {
105+
if (token.content.toLowerCase().includes('new gene')) {
106+
foundNewlyAddedGenes = true;
107+
toRemoveStart = index;
108+
} else if (foundNewlyAddedGenes && token.type === 'bullet_list_close') {
109+
toRemoveEnd = index;
110+
break;
111+
} else if (foundNewlyAddedGenes && token.type === 'inline') {
112+
genes.push(token.content);
113+
}
114+
index++;
115+
}
116+
if (toRemoveStart < 0 && toRemoveEnd < 0) {
117+
return true;
118+
} else if (toRemoveStart < 0 || toRemoveEnd < 0) {
119+
throw new Error(
120+
`Found one remove gene index, but not the other (${toRemoveStart}, ${toRemoveEnd})`
121+
);
122+
}
123+
124+
const alterationPageLinkTags = createMarkdownToken(
125+
md,
126+
'NewlyAddedGenesListItem'
127+
);
128+
alterationPageLinkTags[0].attrSet(
129+
'genes',
130+
`${objectPropertyMark}${JSON.stringify(genes)}${objectPropertyMark}`
131+
);
132+
alterationPageLinkTags[1].tag = 'NewlyAddedGenesListItem';
133+
134+
state.tokens = state.tokens.filter(
135+
(_, idx) => idx < toRemoveStart || idx > toRemoveEnd
136+
);
137+
state.tokens.splice(toRemoveStart, 0, ...alterationPageLinkTags);
138+
139+
return true;
140+
}
141+
142+
/**
143+
* @param {import('markdown-it').StateCore} state - The state object containing Markdown parsing tokens.
144+
* @returns {boolean | undefined} - The modified string with all occurrences replaced.
145+
*
146+
* @throws {Error} If a mutation is found in a row without an associated gene.
147+
*/
148+
function fixLinks(state) {
149+
for (const token of state.tokens) {
150+
if (token.type === 'inline') {
151+
let foundLocalLink = false;
152+
for (const child of token.children) {
153+
if (child.type === 'link_close' && foundLocalLink) {
154+
foundLocalLink = false;
155+
child.tag = 'Link';
156+
continue;
157+
} else if (child.type !== 'link_open') {
158+
continue;
159+
}
160+
const hrefIndex = child.attrIndex('href');
161+
if (hrefIndex < 0) {
162+
continue;
163+
}
164+
const currentUrl = child.attrs[hrefIndex][1];
165+
let replaceUrlString = '';
166+
for (const url of oncokbBaseUrls) {
167+
if (currentUrl.startsWith(url)) {
168+
replaceUrlString = url;
169+
foundLocalLink = true;
170+
break;
171+
}
172+
}
173+
if (foundLocalLink) {
174+
child.tag = 'Link';
175+
const attr = child.attrs[hrefIndex];
176+
attr[0] = 'to';
177+
attr[1] = currentUrl.replace(replaceUrlString, '');
178+
}
179+
}
180+
}
181+
}
182+
}
183+
184+
/**
185+
* @param {import('markdown-it')} md - The markdown-it instance used for token processing.
186+
* @param {import('markdown-it').StateCore} state - The state object containing Markdown parsing tokens.
187+
* @returns {boolean | undefined} - The modified string with all occurrences replaced.
188+
*
189+
* @throws {Error} If a mutation is found in a row without an associated gene.
190+
*/
191+
function addAutoTableLinks(md, state) {
192+
let inTh = false;
193+
let inTd = false;
194+
let columnIdx = 0;
195+
let geneIdx = -1;
196+
let mutationIdx = -1;
197+
let currentGene = '';
198+
for (const token of state.tokens) {
199+
if (token.type === 'th_open') {
200+
inTh = true;
201+
} else if (token.type === 'th_close') {
202+
inTh = false;
203+
columnIdx++;
204+
} else if (token.type === 'td_open') {
205+
inTd = true;
206+
} else if (token.type === 'td_close') {
207+
inTd = false;
208+
columnIdx++;
209+
} else if (token.type === 'tr_open') {
210+
columnIdx = 0;
211+
currentGene = '';
212+
} else if (inTd && columnIdx === geneIdx && token.type === 'inline') {
213+
const child = token.children[0];
214+
currentGene = child.content;
215+
child.content = `{getAlternativeGenePageLinks('${child.content}')}`;
216+
} else if (inTd && columnIdx === mutationIdx && token.type === 'inline') {
217+
const child = token.children[0];
218+
if (currentGene === '') {
219+
throw new Error(`No gene for this row and mutation "${child.content}"`);
220+
}
221+
const alterationPageLinkTags = createMarkdownToken(
222+
md,
223+
'AlterationPageLink'
224+
);
225+
alterationPageLinkTags[0].attrSet('hugoSymbol', currentGene);
226+
alterationPageLinkTags[0].attrSet('alteration', child.content);
227+
token.children = alterationPageLinkTags;
228+
} else if (inTh && token.content === 'Gene') {
229+
geneIdx = columnIdx;
230+
} else if (inTh && token.content === 'Mutation') {
231+
mutationIdx = columnIdx;
232+
}
233+
}
234+
}
235+
236+
/**
237+
* Creates a Markdown token with a specified tag.
238+
*
239+
* @param {import('markdown-it')} md - The markdown-it instance used for parsing.
240+
* @param {string} tag - The tag to set on the first non-text token.
241+
* @returns {import('markdown-it').Token[]} - An array of markdown-it token objects with the modified tag.
242+
*
243+
* @example
244+
* const md = require('markdown-it')();
245+
* const tokens = createMarkdownToken(md, 'custom-tag');
246+
* console.log(tokens);
247+
*/
248+
function createMarkdownToken(md, tag) {
249+
// can't figure out how to make a token on my own so I'm forcing
250+
// markdown-it to make me one
251+
const alterationPageLinkTags = md
252+
.parse('[placeholder](placeholder)', {})[1]
253+
.children.filter(x => x.type !== 'text');
254+
alterationPageLinkTags[0].type = 'para';
255+
alterationPageLinkTags[0].tag = tag;
256+
alterationPageLinkTags[0].attrs = [];
257+
alterationPageLinkTags[1].tag = tag;
258+
return alterationPageLinkTags;
259+
}
260+
261+
const md = new MarkdownIt({
262+
html: true,
263+
linkify: true,
264+
breaks: true,
265+
}).use(md => {
266+
// https://markdown-it.github.io/markdown-it
267+
// https://github.com/markdown-it/markdown-it/blob/master/docs/examples/renderer_rules.md
268+
md.renderer.rules.table_open = function () {
269+
return '<div className="table-responsive">\n<table className="table">';
270+
};
271+
272+
md.renderer.rules.table_close = function () {
273+
return '</table>\n</div>';
274+
};
275+
276+
md.core.ruler.push('add-auto-table-links', state => {
277+
addAutoTableLinks(md, state);
278+
});
279+
280+
md.core.ruler.push('fix-links', state => fixLinks(state));
281+
md.core.ruler.push('fix-styles', state => {
282+
for (const token of state.tokens) {
283+
if (token.attrs != null && token.attrs.length > 0) {
284+
token.attrs = token.attrs.filter(([name]) => name !== 'style');
285+
}
286+
if (token.type === 'table_open') {
287+
token.attrSet('className', 'table');
288+
}
289+
}
290+
return true;
291+
});
292+
md.core.ruler.push('newly-added-genes', state => {
293+
newlyAddedGenes(md, state);
294+
});
295+
});
296+
297+
const args = process.argv.slice(2);
298+
let inputFolder = null;
299+
let outputFolder = null;
300+
301+
for (let i = 0; i < args.length; i++) {
302+
if (args[i] === '-f' && args[i + 1]) {
303+
inputFolder = path.resolve(args[i + 1]);
304+
} else if (args[i] === '-o' && args[i + 1]) {
305+
outputFolder = path.resolve(args[i + 1]);
306+
}
307+
}
308+
309+
if (!inputFolder || !outputFolder) {
310+
console.error(
311+
'Error: Both -f (input folder) and -o (output folder) arguments are required.'
312+
);
313+
process.exit(1);
314+
}
315+
316+
// Ensure input folder exists
317+
if (!fs.existsSync(inputFolder) || !fs.statSync(inputFolder).isDirectory()) {
318+
console.error(
319+
`Error: Input folder "${inputFolder}" does not exist or is not a directory.`
320+
);
321+
process.exit(1);
322+
}
323+
324+
// Ensure output folder exists, or create it
325+
if (!fs.existsSync(outputFolder)) {
326+
fs.mkdirSync(outputFolder, { recursive: true });
327+
}
328+
329+
// Read all Markdown files in the input folder
330+
const files = fs.readdirSync(inputFolder).filter(file => file.endsWith('.md'));
331+
332+
if (files.length === 0) {
333+
console.warn(`Warning: No markdown files found in "${inputFolder}".`);
334+
}
335+
336+
files.forEach(file => {
337+
const filePath = path.join(inputFolder, file);
338+
const content = fs.readFileSync(filePath, 'utf-8');
339+
const htmlContent = fixHtmlString(md.render(content));
340+
341+
const componentName = path
342+
.basename(file, '.md')
343+
.replace(/[^a-zA-Z0-9]/g, '_');
344+
345+
const tsxContent = `import React from 'react';
346+
import { Link } from 'react-router-dom';
347+
import { AlterationPageLink, getAlternativeGenePageLinks } from 'app/shared/utils/UrlUtils';
348+
import { NewlyAddedGenesListItem } from 'app/pages/newsPage/NewlyAddedGenesListItem';
349+
350+
export default function ${componentName}() {
351+
return (
352+
<>
353+
${htmlContent}
354+
</>
355+
);
356+
}`;
357+
358+
const outputFilePath = path.join(outputFolder, `${componentName}.tsx`);
359+
fs.writeFileSync(outputFilePath, tsxContent, 'utf-8');
360+
console.log(`Generated: ${outputFilePath}`);
361+
});

src/main/webapp/app/config/constants.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ export type DataRelease = {
866866
};
867867

868868
export const DATA_RELEASES: DataRelease[] = [
869+
{ date: '02272025', version: 'v4.26' },
869870
{ date: '01302025', version: 'v4.25' },
870871
{ date: '12192024', version: 'v4.24' },
871872
{ date: '11262024', version: 'v4.23' },

0 commit comments

Comments
 (0)