Skip to content

Commit 2309019

Browse files
committed
feat: external http/s linked styles sheets parsing and code hints
1 parent 702bf74 commit 2309019

File tree

3 files changed

+119
-29
lines changed

3 files changed

+119
-29
lines changed

src/language/CSSUtils.js

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121

2222
/*jslint regexp: true */
23-
/*global jsPromise*/
23+
/*global jsPromise, catchToNull, path*/
2424

2525
/**
2626
* Set of utilities for simple parsing of CSS text.
@@ -40,8 +40,9 @@ define(function (require, exports, module) {
4040
IndexingWorker = require("worker/IndexingWorker"),
4141
_ = require("thirdparty/lodash");
4242

43+
const MAX_CONTENT_LENGTH = 10 * 1024 * 1024; // 10MB
4344
// Constants
44-
var SELECTOR = "selector",
45+
const SELECTOR = "selector",
4546
PROP_NAME = "prop.name",
4647
PROP_VALUE = "prop.value",
4748
IMPORT_URL = "import.url";
@@ -1847,7 +1848,7 @@ define(function (require, exports, module) {
18471848
}
18481849
}
18491850

1850-
const MODE_MAP = {
1851+
const CSS_MODE_MAP = {
18511852
css: "CSS",
18521853
less: "LESS",
18531854
scss: "SCSS"
@@ -1866,14 +1867,14 @@ define(function (require, exports, module) {
18661867
return;
18671868
}
18681869
const langID = doc.getLanguage().getId();
1869-
if(!MODE_MAP[langID]){
1870+
if(!CSS_MODE_MAP[langID]){
18701871
console.log("Cannot parse CSS for mode :", langID, "ignoring", fullPath);
18711872
resolve(selectors);
18721873
return;
18731874
}
18741875
console.log("scanning file for css selector collation: ", fullPath);
18751876
IndexingWorker.execPeer("css_getAllSymbols",
1876-
{text: doc.getText(), cssMode: "CSS", filePath: fullPath})
1877+
{text: doc.getText(), cssMode: CSS_MODE_MAP[langID], filePath: fullPath})
18771878
.then((selectorArray)=>{
18781879
selectors = _extractSelectorSet(selectorArray);
18791880
CSSSelectorCache.set(fullPath, selectors);
@@ -1913,34 +1914,99 @@ define(function (require, exports, module) {
19131914
return (_htmlLikeFileExts.indexOf(LanguageManager.getLanguageForPath(fullPath).getId() || "") !== -1);
19141915
}
19151916

1916-
function _getAllSelectorsInCurrentHTMLEditor() {
1917-
return new Promise(resolve=>{
1918-
let selectors = new Set();
1919-
const htmlEditor = EditorManager.getCurrentFullEditor();
1920-
if (!htmlEditor || !_isHtmlLike(htmlEditor) ) {
1921-
resolve(selectors);
1917+
const cacheInProgress = new Set();
1918+
async function _precacheExternalStyleSheet(link) {
1919+
try {
1920+
if(cacheInProgress.has(link)){
1921+
return;
1922+
}
1923+
cacheInProgress.add(link);
1924+
const extension = path.extname(new URL(link).pathname).slice(1);
1925+
if (!extension || !CSS_MODE_MAP[extension]) {
1926+
console.log(`Not a valid stylesheet type ${extension}, ignoring`, link);
1927+
return;
1928+
}
1929+
const responseHead = await fetch(link, { method: 'HEAD' });
1930+
const contentLength = responseHead.headers.get('Content-Length');
1931+
if (contentLength > MAX_CONTENT_LENGTH) {
1932+
console.log(`Stylesheet is larger than ${MAX_CONTENT_LENGTH}bytes, ignoring`, link);
19221933
return;
19231934
}
19241935

1925-
// Find all <style> blocks in the HTML file
1926-
const styleBlocks = HTMLUtils.findStyleBlocks(htmlEditor);
1927-
let cssText = "";
1928-
1929-
styleBlocks.forEach(function (styleBlockInfo) {
1930-
// Search this one <style> block's content
1931-
cssText += styleBlockInfo.text;
1932-
});
1933-
const fullPath = htmlEditor.document.file.fullPath;
1934-
IndexingWorker.execPeer("css_getAllSymbols", {text: cssText, cssMode: "CSS", filePath: fullPath})
1936+
const response = await fetch(link);
1937+
const styleSheetText = await response.text();
1938+
IndexingWorker.execPeer("css_getAllSymbols",
1939+
{text: styleSheetText, cssMode: CSS_MODE_MAP[extension], filePath: link})
19351940
.then((selectorArray)=>{
1936-
selectors = _extractSelectorSet(selectorArray);
1937-
CSSSelectorCache.set(fullPath, selectors);
1938-
resolve(selectors);
1941+
CSSSelectorCache.set(link, _extractSelectorSet(selectorArray));
19391942
}).catch(err=>{
1940-
console.warn("CSS language service unable to get selectors for" + fullPath, err);
1941-
resolve(selectors); // still resolve, so the overall result doesn't reject
1943+
console.warn("CSS language service unable to get selectors for link" + link, err);
19421944
});
1945+
} catch (e) {
1946+
console.error("Error pre caching externally linked style sheet ", link);
1947+
}
1948+
cacheInProgress.delete(link);
1949+
}
1950+
1951+
const MAX_ALLOWED_EXTERNAL_STYLE_SHEETS = 20;
1952+
1953+
/**
1954+
* html files may have embedded link style sheets to external CDN urls. We will parse them to get all selectors.
1955+
* @param htmlFileContent
1956+
* @param fileMode
1957+
* @param fullPath
1958+
* @return {Promise<void>}
1959+
* @private
1960+
*/
1961+
async function _getLinkedCSSFileSelectors(htmlFileContent, fileMode, fullPath) {
1962+
const linkedFiles = await catchToNull(IndexingWorker.execPeer(
1963+
"html_getAllLinks", {text: htmlFileContent, htmlMode: fileMode, filePath: fullPath}),
1964+
"error extracting linked css files from"+ fullPath) || [];
1965+
let selectors = new Set();
1966+
let externalStyleSheetCount = 0;
1967+
for(const link of linkedFiles) {
1968+
if(externalStyleSheetCount >= MAX_ALLOWED_EXTERNAL_STYLE_SHEETS){
1969+
break;
1970+
}
1971+
if(link.startsWith("http://") || link.startsWith("https://")) {
1972+
externalStyleSheetCount ++;
1973+
const cachedSelectors = CSSSelectorCache.get(link);
1974+
if(cachedSelectors){
1975+
cachedSelectors.forEach(value=>{
1976+
selectors.add(value);
1977+
});
1978+
} else {
1979+
_precacheExternalStyleSheet(link);
1980+
}
1981+
}
1982+
}
1983+
return selectors;
1984+
}
1985+
1986+
async function _getAllSelectorsInCurrentHTMLEditor() {
1987+
let selectors = new Set();
1988+
const htmlEditor = EditorManager.getCurrentFullEditor();
1989+
if (!htmlEditor || !_isHtmlLike(htmlEditor) ) {
1990+
return selectors;
1991+
}
1992+
1993+
// Find all <style> blocks in the HTML file
1994+
const styleBlocks = HTMLUtils.findStyleBlocks(htmlEditor);
1995+
let cssText = "";
1996+
1997+
styleBlocks.forEach(function (styleBlockInfo) {
1998+
// Search this one <style> block's content
1999+
cssText += styleBlockInfo.text;
19432000
});
2001+
const fullPath = htmlEditor.document.file.fullPath;
2002+
const selectorsPromise = IndexingWorker.execPeer(
2003+
"css_getAllSymbols", {text: cssText, cssMode: "CSS", filePath: fullPath});
2004+
const htmlLanguageID = LanguageManager.getLanguageForPath(fullPath).getId();
2005+
const remoteLinkedSelectors = await _getLinkedCSSFileSelectors(htmlEditor.document.getText(),
2006+
htmlLanguageID.toUpperCase(), fullPath);
2007+
selectors = await catchToNull(selectorsPromise, "CSS language service unable to get selectors for" + fullPath);
2008+
selectors = selectors || new Set();
2009+
return new Set([...selectors, ...remoteLinkedSelectors]);
19442010
}
19452011

19462012
let globalPrecacheRun = 0;

src/main.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ window.jsPromise = function (jqueryOrJSPromise) {
4343
};
4444
window.deferredToPromise = window.jsPromise;
4545

46+
/**
47+
* A safe way to return null on promise fail. This will never reject or throw.
48+
* @param promise
49+
* @param {string} logError - If a string is passed in, will log the error to console.
50+
* @return {*}
51+
*/
52+
window.catchToNull = function (promise, logError) {
53+
return new Promise(resolve=>{
54+
promise.then(resolve)
55+
.catch((err)=>{
56+
logError && console.error(logError, err);
57+
resolve(null);
58+
});
59+
});
60+
};
61+
4662
// splash screen updates for initial install which could take time, or slow networks.
4763
let trackedScriptCount = 0;
4864
function _setSplashScreenStatusUpdate(message1, message2) {

src/worker/language-service-worker-thread.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,25 @@
1919
*
2020
*/
2121

22-
/*global virtualfs, fs, WorkerComm, CSSLanguageService */
22+
/*global WorkerComm, CSSLanguageService, HTMLLanguageService */
2323

2424
importScripts('../thirdparty/no-minify/language-worker.js');
2525

2626
(function () {
27-
function getAllSymbols({text, cssMode, filePath}) {
27+
function CSSGetAllSymbols({text, cssMode, filePath}) {
2828
if(!CSSLanguageService.CSS_MODES[cssMode]) {
2929
throw new Error("Language mode not supported "+ cssMode);
3030
}
3131
return CSSLanguageService.getAllSymbols(text, cssMode, filePath);
3232
}
3333

34-
WorkerComm.setExecHandler("css_getAllSymbols", getAllSymbols);
34+
function htmlGetAllLinks({text, htmlMode, filePath}) {
35+
if(!HTMLLanguageService.HTML_MODES[htmlMode]) {
36+
throw new Error("Language mode not supported "+ htmlMode);
37+
}
38+
return HTMLLanguageService.getAllDocumentLinks(text, htmlMode, filePath);
39+
}
40+
41+
WorkerComm.setExecHandler("css_getAllSymbols", CSSGetAllSymbols);
42+
WorkerComm.setExecHandler("html_getAllLinks", htmlGetAllLinks);
3543
}());

0 commit comments

Comments
 (0)