diff --git a/edit/autocomplete.js b/edit/autocomplete.js index 74aef528..14da5289 100644 --- a/edit/autocomplete.js +++ b/edit/autocomplete.js @@ -2,6 +2,7 @@ /* global cmFactory */ /* global debounce */// toolbox.js /* global editor */ +/* global linterMan */ /* global prefs */ 'use strict'; @@ -11,30 +12,37 @@ const USO_VAR = 'uso-variable'; const USO_VALID_VAR = 'variable-3 ' + USO_VAR; const USO_INVALID_VAR = 'error ' + USO_VAR; + const rxPROP = /^(prop(erty)?|variable-2)\b/; const rxVAR = /(^|[^-.\w\u0080-\uFFFF])var\(/iyu; const rxCONSUME = /([-\w]*\s*:\s?)?/yu; const cssMime = CodeMirror.mimeModes['text/css']; + const cssGlobalValues = [ + 'inherit', + 'initial', + 'revert', + 'unset', + ]; const docFuncs = addSuffix(cssMime.documentTypes, '('); const {tokenHooks} = cssMime; const originalCommentHook = tokenHooks['/']; const originalHelper = CodeMirror.hint.css || (() => {}); - let cssProps, cssMedia; + let cssMedia, cssProps, cssPropsValues; - const aot = prefs.get('editor.autocompleteOnTyping'); - CodeMirror.defineOption('autocompleteOnTyping', aot, aotToggled); - if (aot) cmFactory.globalSetOption('autocompleteOnTyping', true); + const AOT_ID = 'autocompleteOnTyping'; + const AOT_PREF_ID = 'editor.' + AOT_ID; + const aot = prefs.get(AOT_PREF_ID); + CodeMirror.defineOption(AOT_ID, aot, (cm, value) => { + cm[value ? 'on' : 'off']('changes', autocompleteOnTyping); + cm[value ? 'on' : 'off']('pick', autocompletePicked); + }); + prefs.subscribe(AOT_PREF_ID, (key, val) => cmFactory.globalSetOption(AOT_ID, val), {runNow: aot}); CodeMirror.registerHelper('hint', 'css', helper); CodeMirror.registerHelper('hint', 'stylus', helper); tokenHooks['/'] = tokenizeUsoVariables; - function aotToggled(cm, value) { - cm[value ? 'on' : 'off']('changes', autocompleteOnTyping); - cm[value ? 'on' : 'off']('pick', autocompletePicked); - } - - function helper(cm) { + async function helper(cm) { const pos = cm.getCursor(); const {line, ch} = pos; const {styles, text} = cm.getLineHandle(line); @@ -64,7 +72,7 @@ const str = text.slice(prev, end); const left = text.slice(prev, ch).trim(); let leftLC = left.toLowerCase(); - let list = []; + let list; switch (leftLC[0]) { case '!': @@ -125,8 +133,29 @@ // fallthrough to `default` default: + // property values + if (isStylusLang || getTokenState() === 'prop') { + while (i > 0 && !rxPROP.test(styles[i + 1])) i -= 2; + const propEnd = styles[i]; + let prop; + if (propEnd > text.lastIndexOf(';', ch - 1)) { + while (i > 0 && rxPROP.test(styles[i + 1])) i -= 2; + prop = text.slice(styles[i] || 0, propEnd).match(/([-\w]+)?$/u)[1]; + } + if (prop) { + if (/[^-\w]/.test(leftLC)) { + prev += execAt(/[\s:()]*/y, prev, text)[0].length; + leftLC = leftLC.replace(/^[^\w\s]\s*/, ''); + } + if (prop.startsWith('--')) prop = 'color'; // assuming 90% of variables are colors + if (!cssPropsValues) cssPropsValues = await linterMan.worker.getCssPropsValues(); + list = [...new Set([...cssPropsValues[prop] || [], ...cssGlobalValues])]; + end = prev + execAt(/(\s*[-a-z(]+)?/y, prev, text)[0].length; + } + } // properties and media features - if (/^(prop(erty|\?)|atom|error)/.test(type) && + if (!list && + /^(prop(erty|\?)|atom|error)/.test(type) && /^(block|atBlock_parens|maybeprop)/.test(getTokenState())) { if (!cssProps) initCssProps(); if (type === 'prop?') { @@ -136,7 +165,9 @@ list = state === 'atBlock_parens' ? cssMedia : cssProps; end -= /\W$/u.test(str); // e.g. don't consume ) when inside () end += execAt(rxCONSUME, end, text)[0].length; - } else { + + } + if (!list) { return isStylusLang ? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus}) : originalHelper(cm); diff --git a/edit/editor-worker.js b/edit/editor-worker.js index c1806014..ad150da1 100644 --- a/edit/editor-worker.js +++ b/edit/editor-worker.js @@ -16,6 +16,34 @@ .map(m => Object.assign(m, {rule: {id: m.rule.id}})); }, + getCssPropsValues() { + require(['/js/csslint/parserlib']); /* global parserlib */ + const {css: {Colors, Properties}, util: {describeProp}} = parserlib; + const namedColors = Object.keys(Colors); + const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g; + const res = {}; + // moving vendor-prefixed props to the end + const cmp = (a, b) => a[0] === '-' && b[0] !== '-' ? 1 : a < b ? -1 : a > b; + for (const [k, v] of Object.entries(Properties)) { + if (typeof v === 'string') { + let last = ''; + const uniq = []; + // strip definitions of function arguments + const desc = describeProp(v).replace(/([-\w]+)\(.*?\)/g, 'z-$1'); + const descNoColors = desc.replace(//g, ''); + // add a prefix to functions to group them at the end + const words = descNoColors.split(rxNonWord).sort(cmp); + for (let w of words) { + if (w.startsWith('z-')) w = w.slice(2) + '('; + if (w !== last) uniq.push(last = w); + } + if (desc !== descNoColors) uniq.push(...namedColors); + if (uniq.length) res[k] = uniq; + } + } + return res; + }, + getRules(linter) { return ruleRetriever[linter](); // eslint-disable-line no-use-before-define }, diff --git a/js/csslint/parserlib.js b/js/csslint/parserlib.js index e48b08fa..6f437be4 100644 --- a/js/csslint/parserlib.js +++ b/js/csslint/parserlib.js @@ -34,7 +34,7 @@ self.parserlib = (() => { 'align-items': 'normal | stretch | | [ ? ]', 'align-content': '', 'align-self': '', - 'all': 'initial | inherit | unset', + 'all': 'initial | inherit | revert | unset', 'alignment-adjust': 'auto | baseline | before-edge | text-before-edge | middle | central | ' + 'after-edge | text-after-edge | ideographic | alphabetic | hanging | ' + 'mathematical | ', @@ -745,9 +745,9 @@ self.parserlib = (() => { 'emoji | math | fangsong | ui-serif | ui-sans-serif | ui-monospace | ui-rounded', '': ' | fill-box | stroke-box | view-box', '': p => p.type === 'angle' && p.units === 'deg', - '': p => - p.type === 'function' && - /^(?:-(?:webkit|moz|ms|o)-)?(?:repeating-)?(?:radial-|linear-|conic-)?gradient/i.test(p), + '': 'radial-gradient() | linear-gradient() | conic-gradient() | gradient() | ' + + 'repeating-radial-gradient() | repeating-linear-gradient() | repeating-conic-gradient() | ' + + 'repeating-gradient()', '': p => p.tokenType === Tokens.HASH, //eslint-disable-line no-use-before-define '': 'cielab() | cielch() | cielchab() | icc-color() | icc-named-color()', '': vtIsIdent, @@ -778,7 +778,7 @@ self.parserlib = (() => { '': p => p.value >= 0 && (p.type === 'number' || p.type === 'percentage') || p.isCalc, //eslint-disable-next-line no-use-before-define - '': p => p.text in Colors || lower(p.text) in Colors, + '': p => p.text in Colors || ColorsLC.has(lower(p.text)), '': p => p.type === 'number' || p.isCalc, '': p => p.type === 'number' || p.type === 'percentage' || p.isCalc, '': p => p.type === 'number' && p.value >= 0 && p.value <= 1 || p.isCalc, @@ -979,7 +979,12 @@ self.parserlib = (() => { //#endregion //#region Colors - const Colors = { + const Colors = Object.assign(Object.create(null), { + // 'currentColor' color keyword + // https://www.w3.org/TR/css3-color/#currentcolor + currentColor: '', + transparent: '#0000', + aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', @@ -1128,56 +1133,49 @@ self.parserlib = (() => { whitesmoke: '#f5f5f5', yellow: '#ffff00', yellowgreen: '#9acd32', - // 'currentColor' color keyword - // https://www.w3.org/TR/css3-color/#currentcolor - currentcolor: '', - transparent: '#0000', - // CSS2 system colors - // https://www.w3.org/TR/css3-color/#css2-system - activeborder: '', - activecaption: '', - appworkspace: '', - background: '', - buttonface: '', - buttonhighlight: '', - buttonshadow: '', - buttontext: '', - captiontext: '', - graytext: '', - greytext: '', - highlight: '', - highlighttext: '', - inactiveborder: '', - inactivecaption: '', - inactivecaptiontext: '', - infobackground: '', - infotext: '', - menu: '', - menutext: '', - scrollbar: '', - threeddarkshadow: '', - threedface: '', - threedhighlight: '', - threedlightshadow: '', - threedshadow: '', - window: '', - windowframe: '', - windowtext: '', - - // CSS4 system colors, only additions to the above - // https://drafts.csswg.org/css-color-4/#css-system-colors - activetext: '', - buttonborder: '', - canvas: '', - canvastext: '', - field: '', - fieldtext: '', - linktext: '', - mark: '', - marktext: '', - visitedtext: '', - }; + // old = CSS2 system colors: https://www.w3.org/TR/css3-color/#css2-system + // new = CSS4 system colors: https://drafts.csswg.org/css-color-4/#css-system-colors + ActiveBorder: '', + ActiveCaption: '', + ActiveText: '', // new + AppWorkspace: '', + Background: '', + ButtonBorder: '', // new + ButtonFace: '', // old+new + ButtonHighlight: '', + ButtonShadow: '', + ButtonText: '', // old+new + Canvas: '', // new + CanvasText: '', // new + CaptionText: '', + Field: '', // new + FieldText: '', // new + GrayText: '', // old+new + Highlight: '', // old+new + HighlightText: '', // old+new + InactiveBorder: '', + InactiveCaption: '', + InactiveCaptionText: '', + InfoBackground: '', + InfoText: '', + LinkText: '', // new + Mark: '', // new + MarkText: '', // new + Menu: '', + MenuText: '', + Scrollbar: '', + ThreeDDarkShadow: '', + ThreeDFace: '', + ThreeDHighlight: '', + ThreeDLightShadow: '', + ThreeDShadow: '', + VisitedText: '', // new + Window: '', + WindowFrame: '', + WindowText: '', + }); + const ColorsLC = new Set(Object.keys(Colors).map(lower)); //#endregion //#region Tokens @@ -4175,10 +4173,12 @@ self.parserlib = (() => { if (asText) { return text; } + const m = rxVendorPrefix.exec(name) || []; return SyntaxUnit.addFuncInfo( new SyntaxUnit(text, start, 'function', { expr, - name, + name: m[2] || name, + prefix: m[1] || '', tokenType: Tokens.FUNCTION, })); } @@ -4647,6 +4647,7 @@ self.parserlib = (() => { Colors, Combinator, Parser, + Properties, PropertyName, PropertyValue, PropertyValuePart, @@ -4662,12 +4663,13 @@ self.parserlib = (() => { ValidationError, }, util: { + EventTarget, StringReader, SyntaxError, SyntaxUnit, - EventTarget, TokenStreamBase, rxVendorPrefix, + describeProp: vtExplode, }, cache: parserCache, };