github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/quotestate.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  
     6  	"github.com/xyproto/mode"
     7  )
     8  
     9  // QuoteState keeps track of if we're within a multi-line comment, single quotes, double quotes or multi-line quotes.
    10  // Single line comments are not kept track of in the same way, they can be detected just by checking the current line.
    11  // If one of the ints are > 0, the other ints should not be added to.
    12  // MultiLine comments (/* ... */) are special.
    13  // This could be a flag int instead
    14  type QuoteState struct {
    15  	singleLineCommentMarker            string
    16  	singleLineCommentMarkerRunes       []rune
    17  	doubleQuote                        int
    18  	backtick                           int
    19  	mode                               mode.Mode
    20  	braCount                           int // square bracket count
    21  	parCount                           int // parenthesis count
    22  	singleQuote                        int
    23  	firstRuneInSingleLineCommentMarker rune
    24  	lastRuneInSingleLineCommentMarker  rune
    25  	startedMultiLineComment            bool
    26  	stoppedMultiLineComment            bool
    27  	containsMultiLineComments          bool
    28  	startedMultiLineString             bool
    29  	hasSingleLineComment               bool
    30  	multiLineComment                   bool
    31  	ignoreSingleQuotes                 bool
    32  }
    33  
    34  // NewQuoteState takes a singleLineCommentMarker (such as "//" or "#") and returns a pointer to a new QuoteState struct
    35  func NewQuoteState(singleLineCommentMarker string, m mode.Mode, ignoreSingleQuotes bool) (*QuoteState, error) {
    36  	var q QuoteState
    37  	q.singleLineCommentMarker = singleLineCommentMarker
    38  	q.singleLineCommentMarkerRunes = []rune(singleLineCommentMarker)
    39  	lensr := len(q.singleLineCommentMarkerRunes)
    40  	if lensr == 0 {
    41  		return nil, errors.New("single line comment marker is empty")
    42  	}
    43  	q.firstRuneInSingleLineCommentMarker = q.singleLineCommentMarkerRunes[0]
    44  	q.lastRuneInSingleLineCommentMarker = q.singleLineCommentMarkerRunes[lensr-1]
    45  	q.mode = m
    46  	q.ignoreSingleQuotes = ignoreSingleQuotes
    47  	return &q, nil
    48  }
    49  
    50  // None returns true if we're not within ', "", `, /* ... */ or a single-line quote right now
    51  func (q *QuoteState) None() bool {
    52  	return q.singleQuote == 0 && q.doubleQuote == 0 && q.backtick == 0 && !q.multiLineComment && !q.hasSingleLineComment
    53  }
    54  
    55  // ProcessRune is for processing single runes
    56  func (q *QuoteState) ProcessRune(r, prevRune, prevPrevRune rune) {
    57  	switch r {
    58  	case '`':
    59  		if q.None() {
    60  			q.backtick++
    61  			q.startedMultiLineString = true
    62  		} else {
    63  			q.backtick--
    64  			if q.backtick < 0 {
    65  				q.backtick = 0
    66  			}
    67  		}
    68  	case '"':
    69  		if prevPrevRune == '"' && prevRune == '"' {
    70  			q.startedMultiLineString = q.None()
    71  		} else if prevRune != '\\' {
    72  			if q.None() {
    73  				q.doubleQuote++
    74  			} else {
    75  				q.doubleQuote--
    76  				if q.doubleQuote < 0 {
    77  					q.doubleQuote = 0
    78  				}
    79  			}
    80  		}
    81  	case '\'':
    82  		if prevRune != '\\' {
    83  			if q.ignoreSingleQuotes || q.mode == mode.Lisp || q.mode == mode.Clojure {
    84  				return
    85  			}
    86  			if q.None() {
    87  				q.singleQuote++
    88  			} else {
    89  				q.singleQuote--
    90  				if q.singleQuote < 0 {
    91  					q.singleQuote = 0
    92  				}
    93  			}
    94  		}
    95  	case '*': // support multi-line comments
    96  		if q.mode != mode.Shell && q.mode != mode.Make && q.mode != mode.Just && q.firstRuneInSingleLineCommentMarker != '#' && prevRune == '/' && (prevPrevRune == '\n' || prevPrevRune == ' ' || prevPrevRune == '\t') && q.None() {
    97  			// C-style
    98  			q.multiLineComment = true
    99  			q.startedMultiLineComment = true
   100  		} else if (q.mode == mode.StandardML || q.mode == mode.OCaml || q.mode == mode.Haskell) && prevRune == '(' && q.None() {
   101  			q.parCount-- // Not a parenthesis start after all, but the start of a multi-line comment
   102  			q.multiLineComment = true
   103  			q.startedMultiLineComment = true
   104  		} else if (q.mode == mode.Elm || q.mode == mode.Haskell) && prevRune == '{' && q.None() {
   105  			q.parCount-- // Not a parenthesis start after all, but the start of a multi-line comment
   106  			q.multiLineComment = true
   107  			q.startedMultiLineComment = true
   108  		}
   109  	case '{':
   110  		if q.mode == mode.ObjectPascal && q.None() {
   111  			q.multiLineComment = true
   112  			q.startedMultiLineComment = true
   113  		}
   114  	case '-': // support for HTML-style and XML-style multi-line comments
   115  		if q.mode != mode.Shell && q.mode != mode.Make && q.mode != mode.Just && prevRune == '!' && prevPrevRune == '<' && q.None() {
   116  			q.multiLineComment = true
   117  			q.startedMultiLineComment = true
   118  		} else if (q.mode == mode.Elm || q.mode == mode.Haskell) && prevRune == '{' {
   119  			q.multiLineComment = true
   120  			q.startedMultiLineComment = true
   121  		}
   122  	case q.lastRuneInSingleLineCommentMarker:
   123  		// TODO: Simplify by checking q.None() first, and assuming that the len of the marker is > 1 if it's not 1 since it's not 0
   124  		if !q.multiLineComment && !q.hasSingleLineComment && !q.startedMultiLineString && prevPrevRune != ':' && q.doubleQuote == 0 && q.singleQuote == 0 && q.backtick == 0 {
   125  			switch {
   126  			case len(q.singleLineCommentMarkerRunes) == 1:
   127  				fallthrough
   128  			case q.mode != mode.Shell && q.mode != mode.Make && q.mode != mode.Just && len(q.singleLineCommentMarkerRunes) > 1 && prevRune == q.firstRuneInSingleLineCommentMarker:
   129  				q.hasSingleLineComment = true
   130  				q.startedMultiLineString = false
   131  				q.stoppedMultiLineComment = false
   132  				q.multiLineComment = false
   133  				q.backtick = 0
   134  				q.doubleQuote = 0
   135  				q.singleQuote = 0
   136  				// We're in a single line comment, nothing more to do for this line
   137  				return
   138  			}
   139  		}
   140  		if r != '/' {
   141  			break
   142  		}
   143  		// r == '/'
   144  		fallthrough
   145  	case '/': // support C-style multi-line comments
   146  		if q.mode != mode.Shell && q.mode != mode.Make && q.mode != mode.Just && q.firstRuneInSingleLineCommentMarker != '#' && prevRune == '*' {
   147  			q.stoppedMultiLineComment = true
   148  			q.multiLineComment = false
   149  			if q.startedMultiLineComment {
   150  				q.containsMultiLineComments = true
   151  			}
   152  		}
   153  	case '(':
   154  		if q.None() {
   155  			q.parCount++
   156  		}
   157  	case ';':
   158  		if q.mode == mode.Clojure && prevRune == ';' {
   159  			q.hasSingleLineComment = true
   160  		}
   161  	case ')':
   162  		if (q.mode == mode.StandardML || q.mode == mode.OCaml || q.mode == mode.Haskell) && prevRune == '*' {
   163  			q.stoppedMultiLineComment = true
   164  			q.multiLineComment = false
   165  			if q.startedMultiLineComment {
   166  				q.containsMultiLineComments = true
   167  			}
   168  		} else if q.None() {
   169  			q.parCount--
   170  		}
   171  	case '}':
   172  		if (q.mode == mode.Elm || q.mode == mode.Haskell) && prevRune == '-' {
   173  			q.stoppedMultiLineComment = true
   174  			q.multiLineComment = false
   175  			if q.startedMultiLineComment {
   176  				q.containsMultiLineComments = true
   177  			}
   178  		} else if q.mode == mode.ObjectPascal {
   179  			q.stoppedMultiLineComment = true
   180  			q.multiLineComment = false
   181  			if q.startedMultiLineComment {
   182  				q.containsMultiLineComments = true
   183  			}
   184  		}
   185  	case '[':
   186  		if q.None() {
   187  			q.braCount++
   188  		}
   189  	case ']':
   190  		if q.None() {
   191  			q.braCount--
   192  		}
   193  	case '>': // support HTML-style and XML-style multi-line comments
   194  		if prevRune == '-' && (q.mode == mode.HTML || q.mode == mode.XML) {
   195  			q.stoppedMultiLineComment = true
   196  			q.multiLineComment = false
   197  			if q.startedMultiLineComment {
   198  				q.containsMultiLineComments = true
   199  			}
   200  		}
   201  	}
   202  }
   203  
   204  // Process takes a line of text and modifies the current quote state accordingly,
   205  // depending on which runes are encountered.
   206  func (q *QuoteState) Process(line string) (rune, rune) {
   207  	q.hasSingleLineComment = false
   208  	q.startedMultiLineString = false
   209  	q.stoppedMultiLineComment = false
   210  	q.containsMultiLineComments = false
   211  	prevRune := '\n'
   212  	prevPrevRune := '\n'
   213  	for _, r := range line {
   214  		q.ProcessRune(r, prevRune, prevPrevRune)
   215  		prevPrevRune = prevRune
   216  		prevRune = r
   217  	}
   218  	return prevRune, prevPrevRune
   219  }
   220  
   221  // ParBraCount will count the parenthesis and square brackets for a single line
   222  // while skipping comments and multi-line strings
   223  // and without modifying the QuoteState.
   224  func (q *QuoteState) ParBraCount(line string) (int, int) {
   225  	qCopy := *q
   226  	qCopy.parCount = 0
   227  	qCopy.braCount = 0
   228  	qCopy.Process(line)
   229  	return qCopy.parCount, qCopy.braCount
   230  }