diff --git a/lib/rouge/demos/iecst b/lib/rouge/demos/iecst new file mode 100644 index 0000000000..16a0371a1f --- /dev/null +++ b/lib/rouge/demos/iecst @@ -0,0 +1,21 @@ +// first-order lag derivative term +(* + Parameter K must be set to T / (T + T1) +*) +FUNCTION_BLOCK DT2 + +VAR_INPUT + IN : REAL; + K : REAL; +END_VAR +VAR_OUTPUT + Q : REAL; +END_VAR +VAR + H : REAL := 0.0; +END_VAR + +BEGIN + Q := IN - H; + H := H + K * Q; +END_FUNCTION_BLOCK diff --git a/lib/rouge/guessers/disambiguation.rb b/lib/rouge/guessers/disambiguation.rb index 17b064ef23..e3031f7ca5 100644 --- a/lib/rouge/guessers/disambiguation.rb +++ b/lib/rouge/guessers/disambiguation.rb @@ -140,12 +140,17 @@ def match?(filename) Puppet end - + disambiguate '*.p' do next Prolog if contains?(':-') next Prolog if matches?(/\A\w+(\(\w+\,\s*\w+\))*\./) next OpenEdge end + + disambiguate '*.st' do + next IecST if matches?(/^\s*END_/i) + next Smalltalk + end end end end diff --git a/lib/rouge/lexers/iecst.rb b/lib/rouge/lexers/iecst.rb new file mode 100644 index 0000000000..d1e11969fb --- /dev/null +++ b/lib/rouge/lexers/iecst.rb @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +module Rouge + module Lexers + class IecST < RegexLexer + tag 'iecst' + title "IEC 61131-3 Structured Text" + desc 'Structured text is a programming language for PLCs (programmable logic controllers).' + filenames '*.awl', '*.scl', '*.st' + + mimetypes 'text/x-iecst' + + def self.keywords + blocks = %w( + PROGRAM CONFIGURATION INITIAL_STEP INTERFACE FUNCTION_BLOCK FUNCTION ACTION TRANSITION + TYPE STRUCT STEP NAMESPACE LIBRARY CHANNEL FOLDER RESOURCE + VAR_ACCESS VAR_CONFIG VAR_EXTERNAL VAR_GLOBAL VAR_INPUT VAR_IN_OUT VAR_OUTPUT VAR_TEMP VAR + CONST METHOD PROPERTY + CASE FOR IF REPEAT WHILE + ) + @keywords ||= Set.new %w( + AT BEGIN BY CONSTANT CONTINUE DO ELSE ELSIF EXIT EXTENDS FROM GET GOTO IMPLEMENTS JMP + NON_RETAIN OF PRIVATE PROTECTED PUBLIC RETAIN RETURN SET TASK THEN TO UNTIL USING WITH + __CATCH __ENDTRY __FINALLY __TRY + ) + blocks + blocks.map {|kw| "END_" + kw} + end + + def self.types + @types ||= Set.new %w( + ANY ARRAY BOOL BYTE POINTER STRING + DATE DATE_AND_TIME DT TIME TIME_OF_DAY TOD + INT DINT LINT SINT UINT UDINT ULINT USINT + WORD DWORD LWORD + REAL LREAL + ) + end + + def self.literals + @literals ||= Set.new %w(TRUE FALSE NULL) + end + + def self.operators + @operators ||= Set.new %w(AND EQ EXPT GE GT LE LT MOD NE NOT OR XOR) + end + + state :whitespace do + # Spaces + rule %r/\s+/m, Text + # // Comments + rule %r((//).*$\n?), Comment::Single + # (* Comments *) + rule %r(\(\*.*?\*\))m, Comment::Multiline + # { Comments } + rule %r(\{.*?\})m, Comment::Special + end + + state :root do + mixin :whitespace + + rule %r/'[^']+'/, Literal::String::Single + rule %r/"[^"]+"/, Literal::String::Symbol + rule %r/%[IQM][XBWDL][\d.]*|%[IQ][\d.]*/, Name::Variable::Magic + rule %r/\b(?:D|DT|T|TOD)#[\d_shmd:]*/i, Literal::Date + rule %r/\b(?:16#[\d_a-f]+|0x[\d_a-f]+)\b/i, Literal::Number::Hex + rule %r/\b2#[01_]+/, Literal::Number::Bin + rule %r/(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i, Literal::Number::Float + rule %r/\b[\d.,_]+/, Literal::Number + + rule %r/\b[A-Z_]+\b/i do |m| + name = m[0].upcase + if self.class.keywords.include?(name) + token Keyword + elsif self.class.types.include?(name) + token Keyword::Type + elsif self.class.literals.include?(name) + token Literal + elsif self.class.operators.include?(name) + token Operator + else + token Name + end + end + + rule %r/S?R?:?=>?|&&?|\*\*?|<[=>]?|>=?|[-:^\/+#]/, Operator + rule %r/\b[a-z_]\w*(?=\s*\()/i, Name::Function + rule %r/\b[a-z_]\w*\b/i, Name + rule %r/[()\[\].,;]/, Punctuation + end + end + end +end diff --git a/spec/lexers/iecst_spec.rb b/spec/lexers/iecst_spec.rb new file mode 100644 index 0000000000..ba6c28ea84 --- /dev/null +++ b/spec/lexers/iecst_spec.rb @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +describe Rouge::Lexers::IecST do + let(:subject) { Rouge::Lexers::IecST.new } + + describe 'guessing' do + include Support::Guessing + + it 'guesses by filename' do + assert_guess :filename => 'foo.awl' + assert_guess :filename => 'foo.scl' + assert_guess :filename => 'foo.st', :source => "VAR;\n END_VAR;\n" + end + + it 'guesses by mimetype' do + assert_guess :mimetype => 'text/x-iecst' + end + end +end diff --git a/spec/lexers/smalltalk_spec.rb b/spec/lexers/smalltalk_spec.rb index edf8f79e02..7100372b9a 100644 --- a/spec/lexers/smalltalk_spec.rb +++ b/spec/lexers/smalltalk_spec.rb @@ -8,7 +8,7 @@ include Support::Guessing it 'guesses by filename' do - assert_guess :filename => 'foo.st' + assert_guess :filename => 'foo.st', :source => '' end it 'guesses by mimetype' do diff --git a/spec/visual/samples/iecst b/spec/visual/samples/iecst new file mode 100644 index 0000000000..c8ac2874f1 --- /dev/null +++ b/spec/visual/samples/iecst @@ -0,0 +1,53 @@ +// single line comment +(* + Block comment +*) +FUNCTION_BLOCK SAMPLE + TITLE='Sample function block' + VERSION : '1.0' + AUTHOR : 'Rouge' + FAMILY: 'sample' + +CONST + Thousand : INT := 1_000; + TwoFiftyFive : BYTE := BYTE#255; + InHex : BYTE := 16#FF; + WithType : BYTE := BYTE#16#FF; + InBinary : BYTE := 2#1111_1111; + + PI : REAL := 3.141592; + OneBillion : REAL := 1_000_000_000; + WithExponent : REAL := REAL#1e+9; +END_CONST + +VAR_INPUT + a : REAL; + b : REAL; + limit : REAL; +END_VAR +VAR_OUTPUT + valid : BOOL; +END_VAR + +VAR + last: REAL := 0.0; +END_VAR + +VAR_TEMP + value: REAL; +END_VAR + +BEGIN + IF a > 0 AND b > 0 THEN + value := sum / 2; + ELSIF a > 0 THEN + value := a; + ELSIF b > 0 THEN + value := b; + ELSE + value := last; + END_IF + last := value; + + valid := value < limit; +END_FUNCTION_BLOCK