It turns out, this is a solved problem. Typopunct is already able to use curvy quotes in text and straight quotes inside tags for a variety of modes. I happen to edit HTML in a mode typopunct doesn't cover out of the box, but that's no problem. See typopunct-mode.el:
(defcustom typopunct-mode-exeptions-alist '((sgml-mode . typopunct-point-in-xml-tag-p) (nxml-mode . typopunct-point-in-xml-tag-p) (html-mode . typopunct-point-in-xml-tag-p)) "Alist for mode specific expections. This alist specifies major mode specific expectional cases when the function `typopunct-insert-quotation-mark' should *not* insert typographical quotation marks. Each element is a pair of a major mode (a symbol) and a predicate function that should return non nil, when `typopunct-insert-quotation-mark' should insert an ASCII `\"'." :group 'typopunct :type '(alist :key-type symbol :value-type function))
Great! Typopunct already has a function to check if it's inside an XML-style angle-bracket tag, and is configured to not insert smart quotes if that's the case while in certain modes.
Instead of html-mode, I use web-mode. So, I added the following to my init.el:
(add-to-list 'typopunct-mode-exeptions-alist '(web-mode . typopunct-point-in-xml-tag-p))
...and everything is right in the world. Remember, kids: it's just elisp, and most of the time the problem you're having is a problem someone else has had before.