# frozen_string_literal: true# :markup: markdownrequire"action_dispatch/journey/nfa/dot"moduleActionDispatchmoduleJourney# :nodoc:moduleGTG# :nodoc:classTransitionTable# :nodoc:includeJourney::NFA::Dotattr_reader:memosDEFAULT_EXP=/[^.\/?]+/DEFAULT_EXP_ANCHORED=/\A#{DEFAULT_EXP}\Z/definitialize@stdparam_states={}@regexp_states={}@string_states={}@accepting={}@memos=Hash.new{|h,k|h[k]=[]}enddefadd_accepting(state)@accepting[state]=trueenddefaccepting_states@accepting.keysenddefaccepting?(state)@accepting[state]enddefadd_memo(idx,memo)@memos[idx]<<memoenddefmemo(idx)@memos[idx]enddefeclosure(t)Array(t)enddefmove(t,full_string,start_index,end_index)return[]ift.empty?next_states=[]tok=full_string.slice(start_index,end_index-start_index)token_matches_default_component=DEFAULT_EXP_ANCHORED.match?(tok)t.each{|s,previous_start|ifprevious_start.nil?# In the simple case of a "default" param regex do this fast-path and add all# next states.iftoken_matches_default_component&&states=@stdparam_states[s]states.each{|re,v|next_states<<[v,nil].freezeif!v.nil?}end# When we have a literal string, we can just pull the next stateifstates=@string_states[s]next_states<<[states[tok],nil].freezeunlessstates[tok].nil?endend# For regexes that aren't the "default" style, they may potentially not be# terminated by the first "token" [./?], so we need to continue to attempt to# match this regexp as well as any successful paths that continue out of it.# both paths could be valid.ifstates=@regexp_states[s]slice_start=ifprevious_start.nil?start_indexelseprevious_startendslice_length=end_index-slice_startcurr_slice=full_string.slice(slice_start,slice_length)states.each{|re,v|# if we match, we can try moving past thisnext_states<<[v,nil].freezeif!v.nil?&&re.match?(curr_slice)}# and regardless, we must continue accepting tokens and retrying this regexp. we# need to remember where we started as well so we can take bigger slices.next_states<<[s,slice_start].freezeend}next_statesenddefas_json(options=nil)simple_regexp=Hash.new{|h,k|h[k]={}}@regexp_states.eachdo|from,hash|hash.eachdo|re,to|simple_regexp[from][re.source]=toendend{regexp_states: simple_regexp,string_states: @string_states,stdparam_states: @stdparam_states,accepting: @accepting}enddefto_svgsvg=IO.popen("dot -Tsvg","w+"){|f|f.write(to_dot)f.close_writef.readlines}3.times{svg.shift}svg.join.sub(/width="[^"]*"/,"").sub(/height="[^"]*"/,"")enddefvisualizer(paths,title="FSM")viz_dir=File.join__dir__,"..","visualizer"fsm_js=File.readFile.join(viz_dir,"fsm.js")fsm_css=File.readFile.join(viz_dir,"fsm.css")erb=File.readFile.join(viz_dir,"index.html.erb")states="function tt() { return #{to_json}; }"fun_routes=paths.sample(3).mapdo|ast|ast.filter_map{|n|casenwhenNodes::Symbolcasen.leftwhen":id"thenrand(100).to_swhen":format"then%w{ xml json }.sampleelse"omg"endwhenNodes::Terminalthenn.symbolelsenilend}.joinendstylesheets=[fsm_css]svg=to_svgjavascripts=[states,fsm_js]fun_routes=fun_routesstylesheets=stylesheetssvg=svgjavascripts=javascriptsrequire"erb"template=ERB.newerbtemplate.result(binding)enddef[]=(from,to,sym)to_mappings=states_hash_for(sym)[from]||={}casesymwhenRegexp# we must match the whole string to a token boundaryifsym==DEFAULT_EXPsym=DEFAULT_EXP_ANCHOREDelsesym=/\A#{sym}\Z/endwhenSymbol# account for symbols in the constraints the same as stringssym=sym.to_sendto_mappings[sym]=toenddefstatesss=@string_states.keys+@string_states.values.flat_map(&:values)ps=@stdparam_states.keys+@stdparam_states.values.flat_map(&:values)rs=@regexp_states.keys+@regexp_states.values.flat_map(&:values)(ss+ps+rs).uniqenddeftransitions@string_states.flat_map{|from,hash|hash.map{|s,to|[from,s,to]}}+@stdparam_states.flat_map{|from,hash|hash.map{|s,to|[from,s,to]}}+@regexp_states.flat_map{|from,hash|hash.map{|s,to|[from,s,to]}}endprivatedefstates_hash_for(sym)casesymwhenString,Symbol@string_stateswhenRegexpifsym==DEFAULT_EXP@stdparam_stateselse@regexp_statesendelseraiseArgumentError,"unknown symbol: %s"%sym.classendendendendendend