text: #include <Python.h>
  
  #include "script/syntax.h"
  #include "app/colors.h"
  
  ScriptHighlighter::ScriptHighlighter(QTextDocument* doc)
      : QSyntaxHighlighter(doc)
  {
      PyObject* kwmod = PyImport_ImportModule("keyword");
      PyObject* kwlist = PyObject_GetAttrString(kwmod, "kwlist");
  
      QList<QString> keywords = {"input", "output", "title", "meta"};
  
      // Get all of Python's keywords and add them to a list.
      for (int i=0; i < PyList_Size(kwlist); ++i)
      {
          PyObject* kw = PyList_GetItem(kwlist, i);
  
          wchar_t* w = PyUnicode_AsWideCharString(kw, NULL);
          keywords << QString::fromWCharArray(w);
          PyMem_Free(w);
      }
  
      Py_DECREF(kwlist);
      Py_DECREF(kwmod);
  
      // Make rules for all the Python keywords.
      QTextCharFormat kw_format;
      kw_format.setForeground(Colors::green);
      for (auto k : keywords)
          rules << Rule("\\b" + k + "\\b", kw_format);
  
      QTextCharFormat quote_format;
      quote_format.setForeground(Colors::brown);
      // Triple-quoted (multiline) strings
      // Single-line triple-quoted string
      rules << Rule("'''.*?'''", quote_format);
      rules << Rule("\"\"\".*?\"\"\"", quote_format);
      // Beginning of multiline string
      rules << Rule("'''.*$", quote_format, BASE, MULTILINE_SINGLE);
      rules << Rule("\"\"\".*$", quote_format, BASE, MULTILINE_DOUBLE);
      // End of multiline string
      rules << Rule("^.*'''", quote_format, MULTILINE_SINGLE, BASE);
      rules << Rule("^.*\"\"\"", quote_format, MULTILINE_DOUBLE, BASE);
      // Inside of multiline string
      rules << Rule("^.+$", quote_format, MULTILINE_SINGLE, MULTILINE_SINGLE);
      rules << Rule("^.+$", quote_format, MULTILINE_DOUBLE, MULTILINE_DOUBLE);
  
      // Regular strings
      rules << Rule("\".*?\"", quote_format);
      rules << Rule("'.*?'", quote_format);
  
      // String that can be prepended to a regex to make it detect negative
      // numbers (but not subtraction).  Note that a closing parenthesis is
      // needed and the desired number is the last match group.
      QString neg = "(^|\\*\\*|[(+\\-=*\\/,\\[])([+\\-\\s]*";
  
      QTextCharFormat float_format;
      float_format.setForeground(Colors::yellow);
      rules << Rule(neg + "\\b\\d+\\.\\d*)", float_format);
      rules << Rule(neg + "\\b\\d+\\.\\d*e\\d+)", float_format);
      rules << Rule(neg + "\\b\\d+e\\d+)", float_format);
  
      QTextCharFormat int_format;
      int_format.setForeground(Colors::orange);
      rules << Rule(neg + "\\b\\d+\\b)", int_format);
  
      QTextCharFormat comment_format;
      comment_format.setForeground(Colors::base03);
  
      rules << Rule("#.*", comment_format);
  }
  
  ////////////////////////////////////////////////////////////////////////////////
  
  void ScriptHighlighter::highlightBlock(const QString& text)
  {
      int offset = 0;
      int state = previousBlockState();
  
      while (offset <= text.length())
      {
          int match_start = -1;
          int match_length;
          Rule rule;
  
          for (auto r : rules)
          {
              if (r.state_in != state)
                  continue;
  
              auto match = r.regex.match(text, offset);
              if (!match.hasMatch())
                  continue;
              auto index = match.lastCapturedIndex();
  
              if (match_start == -1 || match.capturedStart(index) < match_start)
              {
                  match_start = match.capturedStart(index);
                  match_length = match.capturedLength(index);
                  rule = r;
              }
          }
  
          if (match_start == -1)
              break;
  
          setFormat(match_start, match_length, rule.format);
          offset = match_start + match_length;
          state = rule.state_out;
      }
  
      setCurrentBlockState(state);
  }
  