;;; rjsx-mode.el --- Real support for JSX -*- lexical-binding: t -*- ;; Copyright (C) 2016 Felipe Ochoa ;; Author: Felipe Ochoa ;; URL: https://github.com/felipeochoa/rjsx-mode/ ;; Package-Version: 20180624.1758 ;; Package-Requires: ((emacs "24.4") (js2-mode "20170504")) ;; Version: 1.1 ;; Keywords: languages ;;; Commentary: ;; Defines a major mode `rjsx-mode' based on `js2-mode' for editing ;; JSX files. `rjsx-mode' extends the parser in `js2-mode' to support ;; the full JSX syntax. This means you get all of the `js2' features ;; plus proper syntax checking and highlighting of JSX code blocks. ;; ;; Some features that this mode adds to js2: ;; ;; - Highlighting JSX tag names and attributes (using the rjsx-tag and ;; rjsx-attr faces) ;; - Highlight undeclared JSX components ;; - Parsing the spread operator {...otherProps} ;; - Parsing && and || in child expressions {cond && } ;; - Parsing ternary expressions {toggle ? : } ;; ;; Additionally, since rjsx-mode extends the js2 AST, utilities using ;; the parse tree gain access to the JSX structure. ;;; Code: ;;;; Basic mode definitions (require 'cl-lib) (require 'js2-mode) (defgroup rjsx-mode nil "Support for JSX." :group 'js2-mode) (defcustom rjsx-max-size-for-frequent-reparse 100000 "Buffers with fewer than this many characters will be parsed more frequently. Set this to 0 to disable the reparsing altogether. The frequent parsing supports the magic `rjsx-electric-lt' and `rjsx-delete-creates-full-tag' behaviors." :group 'rjsx-mode :type 'integer) ;;;###autoload (define-derived-mode rjsx-mode js2-jsx-mode "RJSX" "Major mode for editing JSX files." :lighter ":RJSX" :group 'rjsx-mode) ;;;###autoload (define-minor-mode rjsx-minor-mode "Minor mode for parsing JSX syntax into an AST." :lighter " rjsx" (if rjsx-minor-mode (js2-minor-mode 1) (js2-minor-mode 0))) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.jsx\\'" . rjsx-mode)) (defun rjsx-parse-xml-initializer (orig-fun) "Dispatch the xml parser based on variable `rjsx-mode' being active or not. This function is used to advise `js2-parse-xml-initializer' (ORIG-FUN) using the `:around' combinator. JS2-PARSER is the original XML parser." (if (or (eq major-mode 'rjsx-mode) rjsx-minor-mode) (rjsx-parse-top-xml) (apply orig-fun nil))) (advice-add 'js2-parse-xml-initializer :around #'rjsx-parse-xml-initializer) (defun rjsx-unadvice-js2 () "Remove the rjsx advice on the js2 parser. This will cause rjsx to stop working globally." (advice-remove 'js2-parse-xml-initializer #'rjsx-parse-xml-initializer)) (defface rjsx-tag '((t . (:inherit font-lock-function-name-face))) "`rjsx-mode' face used to highlight JSX tag names." :group 'rjsx-mode) (defface rjsx-attr '((t . (:inherit font-lock-variable-name-face))) "`rjsx-mode' face used to highlight JSX attribute names." :group 'rjsx-mode) (defface rjsx-text '((t . (:inherit font-lock-string-face))) "`rjsx-mode' face used to highlight JSX text." :group 'rjsx-mode) (defface rjsx-tag-bracket-face '((t . (:inherit default))) "`rjsx-mode' face used to highlight `<', `/', and `>'." :group 'rjsx-mode) ;;;; Parser constants struct definitions ;; Token types for XML nodes. We need to re-use some unused values to ;; not mess up the vectors that js2 has set up (defvar rjsx-JSX js2-ENUM_INIT_KEYS) (defvar rjsx-JSX-CLOSE js2-ENUM_INIT_VALUES) (defvar rjsx-JSX-IDENT js2-ENUM_INIT_ARRAY) (defvar rjsx-JSX-MEMBER js2-ENUM_NEXT) (defvar rjsx-JSX-ATTR js2-ENUM_ID) (defvar rjsx-JSX-SPREAD js2-REF_NS_MEMBER) (defvar rjsx-JSX-TEXT js2-ESCXMLTEXT) (defvar rjsx-JSX-EXPRESSION js2-ESCXMLATTR) (dolist (sym '(rjsx-JSX rjsx-JSX-CLOSE rjsx-JSX-IDENT rjsx-JSX-MEMBER rjsx-JSX-ATTR rjsx-JSX-SPREAD rjsx-JSX-TEXT rjsx-JSX-EXPRESSION)) (aset js2-token-names (symbol-value sym) (downcase (substring (symbol-name sym) 5))) (puthash sym (symbol-value sym) js2-token-codes)) (js2-msg "msg.bad.jsx.ident" "invalid JSX identifier") (js2-msg "msg.invalid.jsx.string" "invalid JSX string (cannot contain delimiter in string body)") (js2-msg "msg.mismatched.close.tag" "mismatched closing JSX tag; expected `%s'") (js2-msg "msg.no.gt.in.opener" "missing `>' in opening tag") (js2-msg "msg.no.gt.in.closer" "missing `>' in closing tag") (js2-msg "msg.no.gt.after.slash" "missing `>' after `/' in self-closing tag") (js2-msg "msg.no.rc.after.spread" "missing `}' after spread-prop") (js2-msg "msg.no.value.after.jsx.prop" "missing value after prop `%s'") (js2-msg "msg.no.dots.in.prop.spread" "missing `...' in spread prop") (js2-msg "msg.no.rc.after.expr" "missing `}' after expression") (js2-msg "msg.empty.expr" "empty `{}' expression") (cl-defstruct (rjsx-node (:include js2-node (type rjsx-JSX)) (:constructor nil) (:constructor make-rjsx-node (&key (pos (js2-current-token-beg)) len name rjsx-props kids))) name ; AST node containing the parsed xml name or nil for fragments rjsx-props ; linked list of AST nodes (both attributes and spreads) kids ; linked list of child xml nodes closing-tag) ; AST node with the tag closer (js2--struct-put 'rjsx-node 'js2-visitor 'rjsx-node-visit) (js2--struct-put 'rjsx-node 'js2-printer 'rjsx-node-print) (defun rjsx-node-visit (ast callback) "Visit the `rjsx-node' children of AST, invoking CALLBACK on them." (let ((name (rjsx-node-name ast))) (when name (js2-visit-ast name callback))) (dolist (prop (rjsx-node-rjsx-props ast)) (js2-visit-ast prop callback)) (dolist (prop (rjsx-node-kids ast)) (js2-visit-ast prop callback)) (when (rjsx-node-closing-tag ast) (js2-visit-ast (rjsx-node-closing-tag ast) callback))) (defun rjsx-node-print (node indent-level) "Print the `rjsx-node' NODE at indent level INDENT-LEVEL." (insert (js2-make-pad indent-level) "<") (let ((name-n (rjsx-node-name node))) (when name-n (js2-print-ast name-n 0))) (dolist (attr (rjsx-node-rjsx-props node)) (insert " ") (js2-print-ast attr 0)) (let ((closer (rjsx-node-closing-tag node))) (if (null closer) (insert "/>") (insert ">") (dolist (child (rjsx-node-kids node)) (js2-print-ast child 0)) (js2-print-ast closer indent-level)))) (defun rjsx-node-opening-tag-name (node) "Return a string with NODE's opening tag including any namespace and member operations." (let ((name-n (rjsx-node-name node))) (cond ((null name-n) "") ; fragment ((rjsx-member-p name-n) (rjsx-member-full-name name-n)) ((rjsx-identifier-p name-n) (rjsx-identifier-full-name name-n)) (t "")))) ; js2-error-node (defun rjsx-node-push-prop (n rjsx-prop) "Extend rjsx-node N's rjsx-props with js2-node RJSX-PROP. Sets JSX-PROPS's parent to N." (let ((rjsx-props (rjsx-node-rjsx-props n))) (if rjsx-props (setcdr rjsx-props (nconc (cdr rjsx-props) (list rjsx-prop))) (setf (rjsx-node-rjsx-props n) (list rjsx-prop)))) (js2-node-add-children n rjsx-prop)) (defun rjsx-node-push-child (n kid) "Extend rjsx-node N's children with js2-node KID. Sets KID's parent to N." (let ((kids (rjsx-node-kids n))) (if kids (setcdr kids (nconc (cdr kids) (list kid))) (setf (rjsx-node-kids n) (list kid)))) (js2-node-add-children n kid)) (cl-defstruct (rjsx-closing-tag (:include js2-node (type rjsx-JSX-CLOSE)) (:constructor nil) (:constructor make-rjsx-closing-tag (&key pos len name))) name) ; An rjsx-identifier or rjsx-member node or nil for fragments (js2--struct-put 'rjsx-closing-tag 'js2-visitor 'rjsx-closing-tag-visit) (js2--struct-put 'rjsx-closing-tag 'js2-printer 'rjsx-closing-tag-print) (defun rjsx-closing-tag-visit (ast callback) "Visit the `rjsx-closing-tag' children of AST, invoking CALLBACK on them." (js2-visit-ast (rjsx-closing-tag-name ast) callback)) (defun rjsx-closing-tag-print (node indent-level) "Print the `rjsx-closing-tag' NODE at INDENT-LEVEL." (insert (js2-make-pad indent-level) "")) (defun rjsx-closing-tag-full-name (n) "Return the string with N's fully-namespaced name, or just name if it's not namespaced." (let ((child (rjsx-closing-tag-name n))) (cond ((null child) "") ; fragment ((rjsx-member-p child) (rjsx-member-full-name child)) ((rjsx-identifier-p child) (rjsx-identifier-full-name child)) (t "")))) (cl-defstruct (rjsx-identifier (:include js2-node (type rjsx-JSX-IDENT)) (:constructor nil) (:constructor make-rjsx-identifier (&key (pos (js2-current-token-beg)) len namespace name))) (namespace nil) name) ; js2-name-node (js2--struct-put 'rjsx-identifier 'js2-visitor 'rjsx-identifier-visit) (js2--struct-put 'rjsx-identifier 'js2-printer 'rjsx-identifier-print) (defun rjsx-identifier-visit (n callback) "Visit N's children can call CALLBACK on them." (js2-visit-ast (rjsx-identifier-name n) callback)) (defun rjsx-identifier-print (node indent-level) "Print the `rjsx-identifier' NODE at INDENT-LEVEL." (insert (js2-make-pad indent-level) (rjsx-identifier-full-name node))) (defun rjsx-identifier-full-name (n) "Return the string with N's fully-namespaced name, or just name if it's not namespaced." (if (rjsx-identifier-namespace n) (format "%s:%s" (rjsx-identifier-namespace n) (js2-name-node-name (rjsx-identifier-name n))) (js2-name-node-name (rjsx-identifier-name n)))) (cl-defstruct (rjsx-member (:include js2-node (type rjsx-JSX-MEMBER)) (:constructor nil) (:constructor make-rjsx-member (&key pos len dots-pos idents))) dots-pos ; List of positions of each dot idents) ; List of rjsx-identifier nodes (js2--struct-put 'rjsx-member 'js2-visitor 'rjsx-member-visit) (js2--struct-put 'rjsx-member 'js2-printer 'rjsx-member-print) (defun rjsx-member-visit (n callback) "Visit N's children and call CALLBACK on them." (dolist (ident (rjsx-member-idents n)) (js2-visit-ast ident callback))) (defun rjsx-member-print (node indent-level) "Print the `rjsx-member' NODE at INDENT-LEVEL." (insert (js2-make-pad indent-level) (rjsx-member-full-name node))) (defun rjsx-member-full-name (n) "Return the string with N's combined names together." (mapconcat 'rjsx-identifier-full-name (rjsx-member-idents n) ".")) (cl-defstruct (rjsx-attr (:include js2-node (type rjsx-JSX-ATTR)) (:constructor nil) (:constructor make-rjsx-attr (&key (pos (js2-current-token-beg)) len name value))) name ; a rjsx-identifier value) ; a js2-expression (js2--struct-put 'rjsx-attr 'js2-visitor 'rjsx-attr-visit) (js2--struct-put 'rjsx-attr 'js2-printer 'rjsx-attr-print) (defun rjsx-attr-visit (ast callback) "Visit the `rjsx-attr' children of AST, invoking CALLBACK on them." (js2-visit-ast (rjsx-attr-name ast) callback) (js2-visit-ast (rjsx-attr-value ast) callback)) (defun rjsx-attr-print (node indent-level) "Print the `rjsx-attr' NODE at INDENT-LEVEL." (js2-print-ast (rjsx-attr-name node) indent-level) (unless (js2-empty-expr-node-p (rjsx-attr-value node)) (insert "=") (js2-print-ast (rjsx-attr-value node) 0))) (cl-defstruct (rjsx-spread (:include js2-node (type rjsx-JSX-SPREAD)) (:constructor nil) (:constructor make-rjsx-spread (&key pos len expr))) expr) ; a js2-expression (js2--struct-put 'rjsx-spread 'js2-visitor 'rjsx-spread-visit) (js2--struct-put 'rjsx-spread 'js2-printer 'rjsx-spread-print) (defun rjsx-spread-visit (ast callback) "Visit the `rjsx-spread' children of AST, invoking CALLBACK on them." (js2-visit-ast (rjsx-spread-expr ast) callback)) (defun rjsx-spread-print (node indent-level) "Print the `rjsx-spread' NODE at INDENT-LEVEL." (insert (js2-make-pad indent-level) "{...") (js2-print-ast (rjsx-spread-expr node) 0) (insert "}")) (cl-defstruct (rjsx-wrapped-expr (:include js2-node (type rjsx-JSX-TEXT)) (:constructor nil) (:constructor make-rjsx-wrapped-expr (&key pos len child))) child) (js2--struct-put 'rjsx-wrapped-expr 'js2-visitor 'rjsx-wrapped-expr-visit) (js2--struct-put 'rjsx-wrapped-expr 'js2-printer 'rjsx-wrapped-expr-print) (defun rjsx-wrapped-expr-visit (ast callback) "Visit the `rjsx-wrapped-expr' child of AST, invoking CALLBACK on them." (js2-visit-ast (rjsx-wrapped-expr-child ast) callback)) (defun rjsx-wrapped-expr-print (node indent-level) "Print the `rjsx-wrapped-expr' NODE at INDENT-LEVEL." (insert (js2-make-pad indent-level) "{") (js2-print-ast (rjsx-wrapped-expr-child node) indent-level) (insert "}")) (cl-defstruct (rjsx-text (:include js2-node (type rjsx-JSX-TEXT)) (:constructor nil) (:constructor make-rjsx-text (&key (pos (js2-current-token-beg)) (len (js2-current-token-len)) value))) value) ; a string (js2--struct-put 'rjsx-text 'js2-visitor 'js2-visit-none) (js2--struct-put 'rjsx-text 'js2-printer 'rjsx-text-print) (defun rjsx-text-print (node _indent-level) "Print the `rjsx-text' NODE at INDENT-LEVEL." ;; Text nodes include whitespace (insert (rjsx-text-value node))) ;;;; Recursive descent parsing (defvar rjsx-print-debug-message nil "If t will print out debug messages.") ;(setq rjsx-print-debug-message t) (defmacro rjsx-maybe-message (&rest args) "If debug is enabled, call `message' with ARGS." `(when rjsx-print-debug-message (message ,@args))) (defvar rjsx-text-syntax-table (let ((table (make-syntax-table (standard-syntax-table)))) ;; `js-indent-line' assumes that \n is not whitespace ;; Since it's not a comment delimiter in JSX text, punctuation ;; is the only other (semi) logical choice (modify-syntax-entry ?\n "." table) table)) (js2-deflocal rjsx-in-xml nil "Variable used to track which xml parsing function is the outermost one.") (define-error 'rjsx-eof-while-parsing "RJSX: EOF while parsing") (defmacro rjsx-handling-eof (&rest body) "Execute BODY and return the result of the last form. If BODY signals `rjsx-eof-while-parsing', instead report a syntax error and return a `js2-error-node'." `(let ((beg (js2-current-token-beg))) (condition-case nil (progn ,@body) (rjsx-eof-while-parsing (let ((len (- js2-ts-cursor beg))) (rjsx-maybe-message (format "Handling eof from %d" beg)) (js2-report-error "msg.syntax" nil beg len) (make-js2-error-node :pos beg :len len)))))) (defsubst rjsx-record-tag-bracket-face () "Fontify the current token with `rjsx-tag-bracket-face'." (js2-set-face (js2-current-token-beg) (js2-current-token-end) 'rjsx-tag-bracket-face 'record)) (defun rjsx-parse-top-xml () "Parse a top level XML fragment. This is the entry point when ‘js2-parse-unary-expr’ finds a '<' character" (if rjsx-in-xml (rjsx-parse-xml) (let ((rjsx-in-xml t)) (rjsx-handling-eof (rjsx-parse-xml))))) (defun rjsx-parse-xml () "Parse a complete xml node from start to end tag." (rjsx-record-tag-bracket-face) (let ((pn (make-rjsx-node)) self-closing name-n name-str child child-name-str is-fragment) (rjsx-maybe-message "Starting rjsx-parse-xml after <") (if (setq child (rjsx-parse-empty-tag)) child (if (eq (js2-peek-token) js2-GT) (setq is-fragment t) (setf (rjsx-node-name pn) (setq name-n (rjsx-parse-member-or-ns 'rjsx-tag))) (if (js2-error-node-p name-n) (progn (rjsx-maybe-message "could not parse tag name") (make-js2-error-node :pos (js2-node-pos pn) :len (1+ (js2-node-len name-n)))) (js2-node-add-children pn name-n) (if js2-highlight-external-variables (let ((name-node (rjsx-identifier-name (if (rjsx-member-p name-n) (car (rjsx-member-idents name-n)) name-n))) (case-fold-search nil)) (when (string-match-p "^[[:upper:]]" (js2-name-node-name name-node)) (js2-record-name-node name-node))))) (rjsx-maybe-message "cleared tag name: '%s'" name-str) ;; Now parse the attributes (rjsx-parse-attributes pn) (rjsx-maybe-message "cleared attributes")) (setq name-str (rjsx-node-opening-tag-name pn)) (progn ;; Now parse either a self closing tag or the end of the opening tag (rjsx-maybe-message "next type: `%s'" (js2-peek-token)) (if (setq self-closing (js2-match-token js2-DIV)) (progn (js2-record-text-property (js2-current-token-beg) (js2-current-token-end) 'rjsx-class 'self-closing-slash) (rjsx-record-tag-bracket-face) ;; TODO: How do we un-mark old slashes? (when (js2-must-match js2-GT "msg.no.gt.after.slash" (js2-node-pos pn) (- (js2-current-token-end) (js2-node-pos pn))) (rjsx-record-tag-bracket-face))) (when (js2-must-match js2-GT "msg.no.gt.in.opener" (js2-node-pos pn) (js2-node-len pn)) (rjsx-record-tag-bracket-face))) (rjsx-maybe-message "cleared opener closer, self-closing: %s" self-closing) (if self-closing (setf (js2-node-len pn) (- (js2-current-token-end) (js2-node-pos pn))) (while (not (rjsx-closing-tag-p (setq child (rjsx-parse-child is-fragment)))) ;; rjsx-parse-child calls our scanner, which always moves ;; forward at least one character. If it hits EOF, it ;; signals to our caller, so we don't have to worry about infinite loops here (rjsx-maybe-message "parsed child") (rjsx-node-push-child pn child) (if (= 0 (js2-node-len child)) ; TODO: Does this ever happen? (js2-get-token))) (setq child-name-str (rjsx-closing-tag-full-name child)) (unless (string= name-str child-name-str) (js2-report-error "msg.mismatched.close.tag" name-str (js2-node-pos child) (js2-node-len child))) (rjsx-maybe-message "cleared children for `%s'" name-str) (js2-node-add-children pn child) (setf (rjsx-node-closing-tag pn) child)) (rjsx-maybe-message "Returning completed XML node") (setf (js2-node-len pn) (- (js2-current-token-end) (js2-node-pos pn))) pn)))) (defun rjsx-parse-empty-tag () "Check if we are in an empty tag of the form `' and consume it if so. Returns a `js2-error-node' if we are in one or nil if not." (let ((beg (js2-current-token-beg))) (when (js2-match-token js2-DIV) (if (js2-match-token js2-GT) (progn ; We're in a block, likely created by us in `rjsx-electric-lt' ;; We only highlight the < to reduce the visual impact (js2-report-error "msg.syntax" nil beg 1) (make-js2-error-node :pos beg :len (- (js2-current-token-end) beg))) ;; TODO: This is probably an unmatched closing tag. We should ;; consume it, mark it an error, and move on (js2-unget-token) nil)))) (defun rjsx-parse-attributes (parent) "Parse all attributes, including key=value and {...spread}, and add them to PARENT." ;; Getting this function to not hang in the loop proved tricky. The ;; key is that `rjsx-parse-spread' and `rjsx-parse-single-attr' both ;; return `js2-error-node's if they fail to consume any tokens, ;; which signals to us that we just need to discard one token and ;; keep going. (let (attr (loop-terminators (list js2-DIV js2-GT js2-EOF js2-ERROR))) (while (not (memql (js2-peek-token) loop-terminators)) (rjsx-maybe-message "Starting loop. Next token type: %s\nToken pos: %s" (js2-peek-token) (js2-current-token-beg)) (setq attr (if (js2-match-token js2-LC) (or (rjsx-check-for-empty-curlies t) (prog1 (rjsx-parse-spread) (rjsx-maybe-message "Parsed spread"))) (rjsx-maybe-message "Parsing single attr") (rjsx-parse-single-attr))) (when (js2-error-node-p attr) (js2-get-token)) ; TODO: We should make this conditional on ; `js2-recover-from-parse-errors' (rjsx-node-push-prop parent attr)))) (cl-defun rjsx-check-for-empty-curlies (&optional dont-consume-rc &key check-for-comments warning) "If the following token is '}' set empty curly errors. If DONT-CONSUME-RC is non-nil, the matched right curly token won't be consumed. Returns a `js2-error-node' if the curlies are empty or nil otherwise. If CHECK-FOR-COMMENTS (a &KEY argument) is non-nil, this will check for comments inside the curlies and returns a `js2-empty-expr-node' if any are found. If WARNING (a &key argument) is non-nil, reports the empty curlies as a warning and not an error and also returns a `js2-empty-expr-node'. Assumes the current token is a '{'." (let ((beg (js2-current-token-beg)) end len) (when (js2-match-token js2-RC) (setq end (js2-current-token-end)) (setq len (- end beg)) (when dont-consume-rc (js2-unget-token)) (if check-for-comments (rjsx-maybe-message "Checking for comments between %d and %d" beg end)) (unless (and check-for-comments (dolist (comment js2-scanned-comments) (rjsx-maybe-message "Comment at %d, length=%d" (js2-node-pos comment) (js2-node-len comment)) ;; TODO: IF comments are in reverse document order, we should be able to ;; bail out early and know we didn't find one (when (and (>= (js2-node-pos comment) beg) (<= (+ (js2-node-pos comment) (js2-node-len comment)) end)) (cl-return-from rjsx-check-for-empty-curlies (make-js2-empty-expr-node :pos beg :len (- end beg)))))) (if warning (progn (js2-report-warning "msg.empty.expr" nil beg len) (make-js2-empty-expr-node :pos beg :len (- end beg))) (js2-report-error "msg.empty.expr" nil beg len) (make-js2-error-node :pos beg :len len)))))) (defun rjsx-parse-spread () "Parse an {...props} attribute." (let ((pn (make-rjsx-spread :pos (js2-current-token-beg))) (beg (js2-current-token-beg)) missing-dots expr) (setq missing-dots (not (js2-match-token js2-TRIPLEDOT))) ;; parse-assign-expr will go crazy if we're looking at `} /', so we ;; check for an empty spread first (if (js2-match-token js2-RC) (setq expr (make-js2-error-node :len 1)) (setq expr (js2-parse-assign-expr)) (when (js2-error-node-p expr) (pop js2-parsed-errors))) ; We'll add our own error (unless (or (js2-match-token js2-RC) (js2-error-node-p expr)) (js2-report-error "msg.no.rc.after.spread" nil beg (- (js2-current-token-end) beg))) (setf (rjsx-spread-expr pn) expr) (setf (js2-node-len pn) (- (js2-current-token-end) (js2-node-pos pn))) (js2-node-add-children pn expr) (if (js2-error-node-p expr) (js2-report-error "msg.syntax" nil beg (- (js2-current-token-end) beg)) (when missing-dots (js2-report-error "msg.no.dots.in.prop.spread" nil beg (js2-node-len pn)))) (if (= 0 (js2-node-len pn)) ; TODO: Is this ever possible? (make-js2-error-node :pos beg :len 0) pn))) (defun rjsx-parse-single-attr () "Parse an 'a=b' JSX attribute and return the corresponding XML node." (let ((pn (make-rjsx-attr)) name value beg) (setq name (rjsx-parse-identifier 'rjsx-attr)) ; Won't consume token on error (if (js2-error-node-p name) name (setf (rjsx-attr-name pn) name) (setq beg (js2-node-pos name)) (setf (js2-node-pos pn) beg) (js2-node-add-children pn name) (rjsx-maybe-message "Got the name for the attr: `%s'" (rjsx-identifier-full-name name)) (if (js2-match-token js2-ASSIGN) ; Won't consume on error (progn (rjsx-maybe-message "Matched the equals sign") (if (js2-match-token js2-LC) (setq value (rjsx-parse-wrapped-expr nil t)) (if (js2-match-token js2-STRING) (setq value (rjsx-parse-string)) (js2-report-error "msg.no.value.after.jsx.prop" (rjsx-identifier-full-name name) beg (- (js2-current-token-end) beg)) (setq value (make-js2-error-node :pos beg :len (js2-current-token-len)))))) (setq value (make-js2-empty-expr-node :pos (js2-current-token-end) :len 0))) (rjsx-maybe-message "value type: `%s'" (js2-node-type value)) (setf (rjsx-attr-value pn) value) (setf (js2-node-len pn) (- (js2-node-end value) (js2-node-pos pn))) (js2-node-add-children pn value) (rjsx-maybe-message "Finished single attribute.") pn))) (defun rjsx-parse-wrapped-expr (allow-empty skip-to-rc) "Parse a curly-brace-wrapped JS expression. If ALLOW-EMPTY is non-nil, will warn for empty braces, otherwise will signal a syntax error. If it does not find a right curly and SKIP-TO-RC is non-nil, after the expression, consumes tokens until the end of the JSX node" (rjsx-maybe-message "parsing wrapped expression") (let (pn (beg (js2-current-token-beg)) (child (rjsx-check-for-empty-curlies nil :check-for-comments allow-empty :warning allow-empty))) (if child (if allow-empty (make-rjsx-wrapped-expr :pos beg :len (js2-node-len child) :child child) child) ;; Will be an error node in this case (setq child (js2-parse-assign-expr)) (rjsx-maybe-message "parsed expression, type: `%s'" (js2-node-type child)) (setq pn (make-rjsx-wrapped-expr :pos beg :child child)) (js2-node-add-children pn child) (when (js2-error-node-p child) (pop js2-parsed-errors)) ; We'll record our own message after checking for RC (if (js2-match-token js2-RC) (rjsx-maybe-message "matched } after expression") (rjsx-maybe-message "did not match } after expression") (when skip-to-rc (while (not (memql (js2-get-token) (list js2-RC js2-EOF js2-DIV js2-GT))) (rjsx-maybe-message "Skipped over `%s'" (js2-current-token-string))) (when (memq (js2-current-token-type) (list js2-DIV js2-GT)) (js2-unget-token))) (unless (js2-error-node-p child) (js2-report-error "msg.no.rc.after.expr" nil beg (- (js2-current-token-beg) beg)))) (when (js2-error-node-p child) (js2-report-error "msg.syntax" nil beg (- (js2-current-token-end) beg))) (setf (js2-node-len pn) (- (js2-current-token-end) beg)) pn))) (defun rjsx-parse-string () "Verify that current token is a valid JSX string. Returns a `js2-error-node' if TOKEN-STRING is not a valid JSX string, otherwise returns a `js2-string-node'. (Strings are invalid if they contain the delimiting quote character inside)" (rjsx-maybe-message "Parsing string") (let* ((token (js2-current-token)) (beg (js2-token-beg token)) (len (- (js2-token-end token) beg)) (token-string (js2-token-string token)) ;; JS2 does not include the quote-chars (quote-char (char-before (js2-token-end token)))) (if (cl-position quote-char token-string) (progn (js2-report-error "msg.invalid.jsx.string" nil beg len) (make-js2-error-node :pos beg :len len)) (make-js2-string-node :pos beg :len len :value token-string)))) (cl-defun rjsx-parse-identifier (&optional face &key (allow-ns t)) "Parse a possibly namespaced identifier and fontify with FACE if given. Returns a `js2-error-node' if unable to parse. If the &key argument ALLOW-NS is nil, does not allow namespaced names." (if (js2-must-match-name "msg.bad.jsx.ident") (let ((pn (make-rjsx-identifier)) (beg (js2-current-token-beg)) (name-parts (list (js2-current-token-string))) (allow-colon allow-ns) (continue t) (prev-token-end (js2-current-token-end)) (name-start (js2-current-token-beg)) matched-colon) (while (and continue (or (and (memq (js2-peek-token) (list js2-SUB js2-ASSIGN_SUB)) (prog2 ; Ensure no whitespace between previous name and this dash (js2-get-token) (eq prev-token-end (js2-current-token-beg)) (js2-unget-token))) (and allow-colon (= (js2-peek-token) js2-COLON)))) (if (setq matched-colon (js2-match-token js2-COLON)) (setf (rjsx-identifier-namespace pn) (apply #'concat (nreverse name-parts)) allow-colon nil name-parts (list) name-start nil) (when (= (js2-get-token) js2-ASSIGN_SUB) ; Otherwise it's a js2-SUB (setf (js2-token-end (js2-current-token)) (1- (js2-current-token-end)) (js2-token-type (js2-current-token)) js2-SUB (js2-token-string (js2-current-token)) "-" js2-ts-cursor (1+ (js2-current-token-beg)) js2-ti-lookahead 0)) (push "-" name-parts)) (setq prev-token-end (js2-current-token-end)) (if (js2-match-token js2-NAME) (if (eq prev-token-end (js2-current-token-beg)) (progn (push (js2-current-token-string) name-parts) (setq prev-token-end (js2-current-token-end) name-start (or name-start (js2-current-token-beg)))) (js2-unget-token) (setq continue nil)) (when (= js2-COLON (js2-current-token-type)) (js2-report-error "msg.bad.jsx.ident" nil beg (- (js2-current-token-end) beg))) ;; We only keep going if this is an `ident-ending-with-dash-colon:' (setq continue (and (not matched-colon) (= (js2-peek-token) js2-COLON))))) (when face (js2-set-face beg (js2-current-token-end) face 'record)) (let ((name-node (if name-start (make-js2-name-node :pos name-start :len (- (js2-current-token-end) name-start) :name (apply #'concat (nreverse name-parts))) (make-js2-name-node :pos (js2-current-token-end) :len 0 :name "")))) (setf (js2-node-len pn) (- (js2-current-token-end) beg) (rjsx-identifier-name pn) name-node) (js2-node-add-children pn name-node)) pn) (make-js2-error-node :len (js2-current-token-len)))) (defun rjsx-parse-member-or-ns (&optional face) "Parse a dotted expression or a namespaced identifier and fontify with FACE if given." (let ((ident (rjsx-parse-identifier face))) (cond ((js2-error-node-p ident) ident) ((rjsx-identifier-namespace ident) ident) (t (rjsx-parse-member ident face))))) (defun rjsx-parse-member (ident &optional face) "Parse a dotted member expression starting with IDENT and fontify with FACE. IDENT is the `rjsx-identifier' node for the first item in the member expression. Returns a `js2-error-node' if unable to parse." (let (idents dots-pos pn end) (setq pn (make-rjsx-member :pos (js2-node-pos ident))) (setq end (js2-current-token-end)) (push ident idents) (while (and (js2-match-token js2-DOT) (not (js2-error-node-p ident))) (push (js2-current-token-beg) dots-pos) (setq end (js2-current-token-end)) (setq ident (rjsx-parse-identifier nil :allow-ns nil)) (push ident idents) (unless (js2-error-node-p ident) (setq end (js2-current-token-end))) (js2-node-add-children pn ident)) (apply 'js2-node-add-children pn idents) (setf (rjsx-member-idents pn) (nreverse idents) (rjsx-member-dots-pos pn) (nreverse dots-pos) (js2-node-len pn) (- end (js2-node-pos pn))) (when face (js2-set-face (js2-node-pos pn) end face 'record)) pn)) (defun rjsx-parse-child (expect-fragment) "Parse an XML child node. Child nodes include plain (unquoted) text, other XML elements, and {}-bracketed expressions. Return the parsed child. EXPECT-FRAGMENT if t, indicates that `' should be parsed as a fragment closing node, and not as an empty tag." (let ((tt (rjsx-get-next-xml-token))) (rjsx-maybe-message "child type `%s'" tt) (cond ((= tt js2-LT) (rjsx-maybe-message "xml-or-close") (rjsx-parse-xml-or-closing-tag expect-fragment)) ((= tt js2-LC) (rjsx-maybe-message "parsing expression { %s" (js2-peek-token)) (rjsx-parse-wrapped-expr t nil)) ((= tt rjsx-JSX-TEXT) (rjsx-maybe-message "text node: '%s'" (js2-current-token-string)) (js2-set-face (js2-current-token-beg) (js2-current-token-end) 'rjsx-text 'record) (js2-record-text-property (js2-current-token-beg) (js2-current-token-end) 'syntax-table rjsx-text-syntax-table) (make-rjsx-text :value (js2-current-token-string))) ((= tt js2-ERROR) (make-js2-error-node :len (js2-current-token-len))) (t (error "Unexpected token type: %s" (js2-peek-token)))))) (defun rjsx-parse-xml-or-closing-tag (expect-fragment) "Parse a JSX tag, which could be a child or a closing tag. Return the parsed child, which is a `rjsx-closing-tag' if a closing tag was parsed. EXPECT-FRAGMENT if t, indicates that `' should be parsed as a fragment closing node, and not as an empty tag." (let ((beg (js2-current-token-beg)) pn) (rjsx-record-tag-bracket-face) (if (and (not expect-fragment) (setq pn (rjsx-parse-empty-tag))) pn (if (js2-match-token js2-DIV) (progn (rjsx-record-tag-bracket-face) (if (and expect-fragment (eq (js2-peek-token) js2-GT)) (setq pn (make-rjsx-closing-tag :pos beg)) (setq pn (make-rjsx-closing-tag :pos beg :name (rjsx-parse-member-or-ns 'rjsx-tag))) (js2-node-add-children pn (rjsx-closing-tag-name pn))) (if (js2-must-match js2-GT "msg.no.gt.in.closer" beg (- (js2-current-token-end) beg)) (rjsx-record-tag-bracket-face) (rjsx-maybe-message "missing closing `>'")) (setf (js2-node-len pn) (- (js2-current-token-end) beg)) pn) (rjsx-maybe-message "parsing a child XML item") (rjsx-parse-xml))))) (defun rjsx-get-next-xml-token () "Scan through the XML text and push one token onto the stack." (setq js2-ts-string-buffer nil) ; for recording the text (when (> js2-ti-lookahead 0) (setq js2-ts-cursor (js2-current-token-end)) (setq js2-ti-lookahead 0)) (let ((token (js2-new-token 0)) c) (rjsx-maybe-message "Running the xml scanner") (catch 'return (while t (setq c (js2-get-char)) (rjsx-maybe-message "'%s' (%s)" (if (= c js2-EOF_CHAR) "EOF" (char-to-string c)) c) (cond ((or (= c ?}) (= c ?>)) (js2-set-string-from-buffer token) (setf (js2-token-type token) js2-ERROR) (js2-report-scan-error "msg.syntax" t) (throw 'return js2-ERROR)) ((or (= c ?<) (= c ?{)) (js2-unget-char) (if js2-ts-string-buffer (progn (js2-set-string-from-buffer token) (setf (js2-token-type token) rjsx-JSX-TEXT) (rjsx-maybe-message "created rjsx-JSX-TEXT token: `%s'" (js2-token-string token)) (throw 'return rjsx-JSX-TEXT)) (js2-get-char) (js2-set-string-from-buffer token) (setf (js2-token-type token) (if (= c ?<) js2-LT js2-LC)) (setf (js2-token-string token) (string c)) (throw 'return (js2-token-type token)))) ((= c js2-EOF_CHAR) (js2-set-string-from-buffer token) (rjsx-maybe-message "Hit EOF. Current buffer: `%s'" (js2-token-string token)) (setf (js2-token-type token) js2-ERROR) (rjsx-maybe-message "Scanner hit EOF. Panic!") (signal 'rjsx-eof-while-parsing nil)) (t (js2-add-to-string c))))))) (js2-deflocal rjsx-buffer-chars-modified-tick 0 "Variable holding the last per-buffer value of `buffer-chars-modified-tick'.") (defun rjsx-maybe-reparse () "Called before accessing the parse tree. For small buffers, will do an immediate reparse to ensure the parse tree is up to date." (when (and (<= (point-max) rjsx-max-size-for-frequent-reparse) (/= rjsx-buffer-chars-modified-tick (buffer-chars-modified-tick))) (js2-reparse) (setq rjsx-buffer-chars-modified-tick (buffer-chars-modified-tick)))) (defun rjsx--tag-at-point () "Return the JSX tag at point, if any, or nil." (rjsx-maybe-reparse) (let ((node (js2-node-at-point (point) t))) (while (and node (not (rjsx-node-p node))) (setq node (js2-node-parent node))) node)) ;;;; Interactive commands and keybindings (defun rjsx-electric-lt (n) "Insert a context-sensitive less-than sign. Optional prefix argument N indicates how many signs to insert. If N is greater than one, no special handling takes place. Otherwise, if the less-than sign would start a JSX block, it inserts `' and places the cursor inside the new tag." (interactive "p") (if (/= n 1) (insert (make-string n ?<)) (if (save-excursion (forward-comment most-negative-fixnum) (skip-chars-backward "\n\r") (or (= (point) (point-min)) (memq (char-before) (append "=(?:>}&|{," nil)) (let ((start (- (point) 6))) (and (>= start (point-min)) (string= (buffer-substring start (point)) "return"))))) (progn (insert "") (backward-char 2)) (insert "<")))) (define-key rjsx-mode-map "<" 'rjsx-electric-lt) (defun rjsx-expand-self-closing-tag (node) "Expand NODE into a balanced tag. Assumes NODE is self-closing `rjsx-node', and that point is at the self-closing slash." (delete-char 1) (search-forward ">") (save-excursion (insert ""))) (defun rjsx-electric-gt (n) "Insert a context-sensitive greater-than sign. Optional prefix argument N indicates how many signs to insert. If N is greater than one, no special handling takes place. Otherwise, if point is in a self-closing JSX tag just before the slash, it creates a matching end-tag and places point just inside the tags." (interactive "p") (if (or (/= n 1) (not (eq (get-char-property (point) 'rjsx-class) 'self-closing-slash))) (insert (make-string n ?>)) (let ((node (rjsx--tag-at-point))) (if node (rjsx-expand-self-closing-tag node) (insert (make-string n ?>)))))) (define-key rjsx-mode-map ">" 'rjsx-electric-gt) (defun rjsx-delete-creates-full-tag (n &optional killflag) "N and KILLFLAG are as in `delete-char'. If N is 1 and KILLFLAG nil, checks to see if we're in a self-closing tag about to delete the slash. If so, deletes the slash and inserts a matching end-tag." (interactive "p") (if (or killflag (/= 1 n) (not (eq (get-char-property (point) 'rjsx-class) 'self-closing-slash))) (if (called-interactively-p 'any) (call-interactively 'delete-forward-char) (delete-char n killflag)) (let ((node (rjsx--tag-at-point))) (if node (rjsx-expand-self-closing-tag node) (delete-char 1))))) (define-key rjsx-mode-map (kbd "C-d") 'rjsx-delete-creates-full-tag) (defun rjsx-rename-tag-at-point (new-name) "Prompt for a new name and modify the tag at point. NEW-NAME is the name to give the tag." (interactive "sNew tag name: ") (let* ((tag (rjsx--tag-at-point)) (closer (and tag (rjsx-node-closing-tag tag)))) (cond ((null tag) (message "No JSX tag found at point")) ((null (rjsx-node-name tag)) ; fragment (cl-assert closer nil "Fragment has no closing-tag") (save-excursion (goto-char (+ 2 (js2-node-abs-pos closer))) (insert new-name) (goto-char (1+ (js2-node-abs-pos tag))) (insert new-name)) (js2-reparse)) (t (let* ((head (rjsx-node-name tag)) (tail (when closer (rjsx-closing-tag-name closer))) beg end) (dolist (part (if tail (list tail head) (list head))) (setq beg (js2-node-abs-pos part) end (+ beg (js2-node-len part))) (delete-region beg end) (save-excursion (goto-char beg) (insert new-name))) (js2-reparse)))))) (define-key rjsx-mode-map (kbd "C-c C-r") 'rjsx-rename-tag-at-point) (provide 'rjsx-mode) ;;; rjsx-mode.el ends here ;; Local Variables: ;; outline-regexp: ";;;\\(;* [^ ;; ]\\|###autoload\\)\\|(....." ;; End: