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
'((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 `\"'."
: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:
'(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.