Cleanup _DC/Select.ReplaceOptions

Cleanup _DC/Select.ReplaceOptions

:class Select : #._html.select
⍝ Description:: Dyalog Enhanced HTML select
⍝ Constructor:: [options [[selected] [[disabled] [prompt]]]]
⍝ options   - vector of options or 2 column matrix of displayed[;1] and returned[;2] values
⍝ selected  - Boolean or integer array indicating pre-selected options(s)
⍝ disabled  - Boolean or integer array indicating disabled options(s)
⍝ prompt    - first item to display (has no value) (default '[Select]')
⍝
⍝ Public Fields::
⍝ Options   - vector of options or 2 column matrix of displayed[;1] and returned[;2] values
⍝ Selected  - Boolean or integer array indicating pre-selected options(s)
⍝ Disabled  - Boolean or integer array indicating disabled options(s)
⍝ Prompt      - first item to display (has no value) (default '[Select]')
⍝
⍝ Public Methods::
⍝ ReplaceOptions options [[selected] [disabled]]
⍝ Arguments as discussed above
⍝
⍝ Examples::
⍝ Select  ('Choice 1' 'Choice 2' 'Choice 3')
⍝ Select  (3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3')
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') 2) ⍝ second item is selected
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') (0 1 0)) ⍝ second item is selected
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') 2 3 'Pick One') ⍝ second item is selected, third item is disabled
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') (2 3) 1) ⍝ second and third items are selected, first item is disabled

    :field public Options←0 2⍴⊂''     ⍝ vector or matrix [;1] display, [;2] value
    :field public Selected←⍬          ⍝ either Boolean integer vector indicating
    :field public Prompt←'[Select]'   ⍝ character vector "Prompt" choice - the first choice on the list and does not have a value
    :field public Disabled←⍬          ⍝ either Boolean integer vector indicating

    ∇ make
      :Access public
      :Implements constructor
    ∇

    ∇ make1 args;attr
      :Access public
      :Implements constructor
      args←eis args
      :If (|≡args)∊0 1 2
      :AndIf ~0∊⍴⊃args
          args←,⊂args
      :EndIf
      (Options Selected Disabled Prompt)←args defaultArgs Options Selected Disabled Prompt
    ∇

    ∇ r←Render;opts;sel
      :Access public

      Content←opts←''
      SetInputName
      Add BuildOptions(Options Selected Disabled)

      r←⎕BASE.Render
    ∇

    ∇ r←BuildOptions(opts sel dis);v
      r←''
      :If ~0∊⍴opts
          opts←eis opts
          :If 1=⍴⍴opts
              opts←opts,⍪opts
          :EndIf
      :EndIf

      v←⍳⍬⍴⍴opts
      (sel dis)←v∘∊∘{∧/⍵∊0 1:⍵/⍳⍴⍵ ⋄ ⍵}∘,¨sel dis
      :If 1<+/sel                         ⍝ if we have multiple items selected
      :AndIf 0∊⍴⊃Attrs[⊂'multiple']       ⍝ and the multiple attribute is not set
          Attrs[⊂'multiple']←⊂'multiple'  ⍝ then set it
      :EndIf

      :If ~0∊⍴Prompt
      :AndIf 0∊⍴⊃Attrs[⊂'multiple']  ⍝ prompt makes no sense if multiple selections are allowed
          r,←(('disabled="disabled" ',(~∨/sel)/'selected="selected"')New #._html.option Prompt).Render
      :EndIf

      r,←FormatOptions(opts sel dis)
    ∇

    ∇ r←FormatOptions(opts sel dis);o;s;d
      :Access Public Shared
      r←''
      :If 1=⍴⍴opts ⋄ opts←opts,⍪opts ⋄ :EndIf
      :For o s d :InEach (↓opts)sel dis
          r,←'<option',({⍵ ine' value="',(HtmlSafeText ⍵),'"'}2⊃o),(s/' selected="selected"'),(d/' disabled="disabled"'),'>',(1⊃o),'</option>'
      :EndFor
    ∇

    ∇ r←{selector}ReplaceOptions args;sel;opts;dis;prompt
    ⍝ Replaces select elements options - used by callback functions
    ⍝ Ex: r←Execute ReplaceOptions ('New Option 1' 'New Option 2') 1
    ⍝ arg = options [[selected] [disabled] [prompt]]
      :Access public
      :If 0=⎕NC'selector' ⋄ selector←'#',id ⋄ :EndIf
      args←eis args
      :If (|≡args)∊0 1 2
      :AndIf ~0∊⍴⊃args
          args←,⊂args
      :EndIf
      (opts sel dis prompt)←args defaultArgs Options ⍬ ⍬ Prompt
      r←selector #.JQ.Replace BuildOptions(opts sel dis)
    ∇
:endclass

:class Select : #._html.select
⍝ Description:: Dyalog Enhanced HTML select
⍝ Constructor:: [options [[selected] [[disabled] [prompt]]]]
⍝ options   - vector of options or 2 column matrix of displayed[;1] and returned[;2] values
⍝ selected  - Boolean or integer array indicating pre-selected options(s)
⍝ disabled  - Boolean or integer array indicating disabled options(s)
⍝ prompt    - first item to display (has no value) (default '[Select]')
⍝
⍝ Public Fields::
⍝ Options   - vector of options or 2 column matrix of displayed[;1] and returned[;2] values
⍝ Selected  - Boolean or integer array indicating pre-selected options(s)
⍝ Disabled  - Boolean or integer array indicating disabled options(s)
⍝ Prompt      - first item to display (has no value) (default '[Select]')
⍝
⍝ Public Methods::
⍝ ReplaceOptions options [[selected] [disabled]]
⍝ Arguments as discussed above
⍝
⍝ Examples::
⍝ Select  ('Choice 1' 'Choice 2' 'Choice 3')
⍝ Select  (3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3')
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') 2) ⍝ second item is selected
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') (0 1 0)) ⍝ second item is selected
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') 2 3 'Pick One') ⍝ second item is selected, third item is disabled
⍝ Select ((3 2⍴'One' 'c1' 'Two' 'c2' 'Three' 'c3') (2 3) 1) ⍝ second and third items are selected, first item is disabled

    :field public Options←0 2⍴⊂''     ⍝ vector or matrix [;1] display, [;2] value
    :field public Selected←⍬          ⍝ either Boolean integer vector indicating
    :field public Prompt←'[Select]'   ⍝ character vector "Prompt" choice - the first choice on the list and does not have a value
    :field public Disabled←⍬          ⍝ either Boolean integer vector indicating

    ∇ make
      :Access public
      :Implements constructor
    ∇

    ∇ make1 args;attr
      :Access public
      :Implements constructor
      args←eis args
      :If (|≡args)∊0 1 2
      :AndIf ~0∊⍴⊃args
          args←,⊂args
      :EndIf
      (Options Selected Disabled Prompt)←args defaultArgs Options Selected Disabled Prompt
    ∇

    ∇ r←Render;opts;sel
      :Access public

      Content←opts←''
      SetInputName
      Add BuildOptions(Options Selected Disabled)

      r←⎕BASE.Render
    ∇

    ∇ r←BuildOptions(opts sel dis);v
      r←''
      :If ~0∊⍴opts
          opts←eis opts
          :If 1=⍴⍴opts
              opts←opts,⍪opts
          :EndIf
      :EndIf

      v←⍳⍬⍴⍴opts
      (sel dis)←v∘∊∘{∧/⍵∊0 1:⍵/⍳⍴⍵ ⋄ ⍵}∘,¨sel dis
      :If 1<+/sel                         ⍝ if we have multiple items selected
      :AndIf 0∊⍴⊃Attrs[⊂'multiple']       ⍝ and the multiple attribute is not set
          Attrs[⊂'multiple']←⊂'multiple'  ⍝ then set it
      :EndIf

      :If ~0∊⍴Prompt
      :AndIf 0∊⍴⊃Attrs[⊂'multiple']  ⍝ prompt makes no sense if multiple selections are allowed
          r,←(('disabled="disabled" ',(~∨/sel)/'selected="selected"')New #._html.option Prompt).Render
      :EndIf

      r,←FormatOptions(opts sel dis)
    ∇

    ∇ r←FormatOptions(opts sel dis);o;s;d
      :Access Public Shared
      r←''
      :If 1=⍴⍴opts ⋄ opts←opts,⍪opts ⋄ :EndIf
      :For o s d :InEach (↓opts)sel dis
          r,←'<option',({⍵ ine' value="',(HtmlSafeText ⍵),'"'}2⊃o),(s/' selected="selected"'),(d/' disabled="disabled"'),'>',(1⊃o),'</option>'
      :EndFor
    ∇

    ∇ r←{selector}ReplaceOptions args;sel;opts;dis
⍝     Replaces select elements options - used by callback functions
⍝ Ex: r←Execute ReplaceOptions ('New Option 1' 'New Option 2') 1
⍝     arg = options [[selected] [disabled]]
      :Access public
      :If 0=⎕NC'selector' ⋄ selector←'#',id ⋄ :EndIf
      args←eis args
      :If (|≡args)∊0 1 2
      :AndIf ~0∊⍴⊃args
          args←,⊂args
      :EndIf
      (opts sel dis)←args defaultArgs Options ⍬ ⍬
      r←selector #.JQ.Replace BuildOptions(opts sel dis)
    ∇
:endclass


fixed #14, added ⍺⊂⍵ (Split)

fixed #14, added ⍺⊂⍵ (Split)

⍬←()
⍝ ⍬     <=> 0⍴0
⍝ ⍴⍬    <=> ,0
⍝# ⍬←5   !!!
⍝ ⍳0    <=> ⍬
⍝ ⍴0    <=> ⍬
⍝ ⍬     <=> ⍬
⍝ ⍬⍬    <=> ⍬ ⍬
⍝ 1⍬2⍬3 <=> 1 ⍬ 2 ⍬ 3

~←~⍠{(~⍺∊⍵)/⍺}
⍝ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"~"AEIOU" <=> 'BCDFGHJKLMNPQRSTVWXYZ'
⍝ 1 2 3 4 5 6~2 4 6                    <=> 1 3 5
⍝ "THIS IS TEXT"~" "                   <=> 'THISISTEXT'
⍝ "THIS" "AND" "THAT"~"T"              <=> 'THIS' 'AND' 'THAT'
⍝ "THIS" "AND" "THAT"~"AND"            <=> 'THIS' 'AND' 'THAT'
⍝ "THIS" "AND" "THAT"~⊂"AND"           <=> 'THIS' 'THAT'
⍝ "THIS" "AND" "THAT"~"TH" "AND"       <=> 'THIS' 'THAT'
⍝ 11 12 13 14 15 16~2 3⍴1 2 3 14 5 6   <=> 11 12 13 15 16
⍝ (2 2⍴⍳4)~2 !!! RANK ERROR

_hook←{⍵⍶⍹⍵;⍺⍶⍹⍵}
⍝ (+÷)\3 7 16 ¯294 <=> (3 3.142857142857143 3.1415929203539825 3.141592653921421)
⍝ (=⌊)123 <=> 1
⍝ (=⌊)123.4 <=> 0
⍝ (÷⍟)1000 <=> 144.76482730108395

⊃←{
  0=⍴⍴⍵:↑⍵
  0=×/⍴⍵:⍵
  shape←⍴⍵ ⋄ ⍵←,⍵
  r←⌈/≢¨shapes←⍴¨⍵ ⍝ maximum rank of all shapes
  max←↑⌈/shapes←(⍴ ↓ (r⍴1)∘,)¨shapes ⍝ maximum shape of rank adjusted shapes
  (shape,max)⍴↑⍪/shapes{max↑⍺⍴⍵}¨⍵
  ;
  1<⍴⍴⍺:↗'RANK ERROR'
  x←⍵
  {
    1<⍴⍴⍵:↗'RANK ERROR'
    ⍵←,⍵
    (⍴⍵)≠⍴⍴x:↗'RANK ERROR'
    ∨/⍵≥⍴x:↗'INDEX ERROR'
    x←⊃⍵⌷x
  }¨⍺
  x
}
⍝ ⊃3            <=> 3
⍝ ⊃(1 2)(3 4)   <=> 2 2⍴1 2 3 4
⍝ ⊃(1 2)(3 4 5) <=> 2 3⍴1 2 0 3 4 5
⍝ ⊃1 2          <=> 1 2
⍝ ⊃(1 2)3       <=> 2 2⍴1 2 3 0
⍝ ⊃1(2 3)       <=> 2 2⍴1 0 2 3
⍝ ⊃2 2⍴1(1 1 2⍴3 4)(5 6)(2 0⍴0) <=> 2 2 1 2 2⍴1 0 0 0 3 4 0 0 5 6 0 0 0 0 0 0
⍝ ⊃⍬            <=> ⍬
⍝ ⊃2 3 0⍴0      <=> 2 3 0⍴0
⍝ ⍬⊃3               <=> 3
⍝ 2⊃'PICK'          <=> 'C'
⍝ (⊂1 0)⊃2 2⍴'ABCD' <=> 'C'
⍝ 1⊃'foo' 'bar'     <=> 'bar'
⍝ 1 2⊃'foo' 'bar'   <=> 'r'
⍝ (2 2⍴0)⊃1 2       !!! RANK ERROR
⍝ (⊂2 1⍴0)⊃2 2⍴0    !!! RANK ERROR
⍝ (⊂2 2⍴0)⊃1 2      !!! RANK ERROR
⍝ (⊂2 2)⊃1 2        !!! RANK ERROR
⍝ (⊂0 2)⊃2 2⍴'ABCD' !!! INDEX ERROR

⊂←⊂⍠{
  ⍝ Split ⍺ into enclosed vectors at each ⍵
  ⍵←⍺=⍵
  ⍝ Like dyadic ⊂, but empties remain (as in Javascript.split)
  ⍵←(⍴⍵),⍨⍵/⍳⍴⍵ ⋄ drop←0
  ⍵{∆←drop↓⍺↑⍵ ⋄ drop←⍺+1 ⋄ ∆}¨⊂⍺
}
⍝ ' this  is a test'⊂' ' <=> '' 'this' '' 'is' (,'a') 'test'

↓←{
  0=⍴⍴⍵:⍵ ⋄ ⊂[¯1+⍴⍴⍵]⍵
  ;
  1<⍴⍴⍺:↗'RANK ERROR'
  a←,⍺
  ⍵←{0=⍴⍴⍵:((⍴a)⍴1)⍴⍵⋄⍵}⍵
  (⍴a)>⍴⍴⍵:↗'RANK ERROR'
  a←(⍴⍴⍵)↑a
  a←((a>0)×0⌊a-⍴⍵)+(a≤0)×0⌈a+⍴⍵
  a↑⍵
}
⍝ ↓1 2 3 <=> ⊂1 2 3
⍝ ↓(1 2)(3 4) <=> ⊂(1 2)(3 4)
⍝ ↓2 2⍴⍳4 <=> (0 1)(2 3)
⍝ ↓2 3 4⍴⍳24 <=> 2 3⍴(0 1 2 3)(4 5 6 7)(8 9 10 11)(12 13 14 15)(16 17 18 19)(20 21 22 23)
⍝ 4↓'OVERBOARD'         <=> 'BOARD'
⍝ ¯5↓'OVERBOARD'        <=> 'OVER'
⍝ ⍴10↓'OVERBOARD'       <=> ,0
⍝ 0 ¯2↓3 3⍴'ONEFATFLY'  <=> 3 1⍴'OFF'
⍝ ¯2 ¯1↓3 3⍴'ONEFATFLY' <=> 1 2⍴'ON'
⍝ 1↓3 3⍴'ONEFATFLY'     <=> 2 3⍴'FATFLY'
⍝ ⍬↓3 3⍴⍳9              <=> 3 3⍴⍳9
⍝ 1 1↓2 3 4⍴"ABCDEFGHIJKLMNOPQRSTUVWXYZ"   <=> 1 2 4⍴'QRSTUVWX'
⍝ ¯1 ¯1↓2 3 4⍴"ABCDEFGHIJKLMNOPQRSTUVWXYZ" <=> 1 2 4⍴'ABCDEFGH'
⍝ 1↓0                   <=> ⍬
⍝ 0 1↓2                 <=> 1 0⍴0
⍝ 1 2↓3                 <=> 0 0⍴0
⍝ ⍬↓0                   <=> 0

⍪←{(≢⍵)(×/1↓⍴⍵)⍴⍵; ⍺,[0]⍵}
⍝ ⍪2 3 4 <=> 3 1⍴2 3 4
⍝ ⍪0 <=> 1 1⍴0
⍝ ⍪2 2⍴2 3 4 5 <=> 2 2⍴2 3 4 5
⍝ ⍴⍪2 3⍴⍳6 <=> 2 3
⍝ ⍴⍪2 3 4⍴⍳24 <=> 2 12
⍝ (2 3⍴⍳6)⍪9 <=> 3 3⍴(0 1 2 3 4 5 9 9 9)
⍝ 1⍪2 <=> 1 2

⊢←{⍵}
⍝ 123⊢456 <=> 456
⍝ ⊢456 <=> 456

⊣←{;⍺}
⍝ 123⊣456 <=> 123
⍝ ⊣456 <=> ⍬

≢←{⍬⍴(⍴⍵),1; ~⍺≡⍵}
⍝ ≢0 <=> 1
⍝ ≢0 0 0 <=> 3
⍝ ≢⍬ <=> 0
⍝ ≢2 3⍴⍳6 <=> 2
⍝ 3≢3 <=> 0

,←{(×/⍴⍵)⍴⍵}⍠,
⍝ ,2 3 4⍴'abcdefghijklmnopqrstuvwx' <=> 'abcdefghijklmnopqrstuvwx'
⍝ ,123 <=> 1⍴123

⌹←{
  norm←{(⍵+.×+⍵)*0.5}

  QR←{ ⍝ QR decomposition
    n←(⍴⍵)[1]
    1≥n:{t←norm,⍵ ⋄ (⍵÷t)(⍪t)}⍵
    m←⌈n÷2
    a0←((1↑⍴⍵),m)↑⍵
    a1←(0,m)↓⍵
    (q0 r0)←∇a0
    c←(+⍉q0)+.×a1
    (q1 r1)←∇a1-q0+.×c
    (q0,q1)((r0,c)⍪((⌊n÷2),-n)↑r1)
  }

  Rinv←{ ⍝ Inverse of an upper triangular matrix
    1=n←1↑⍴⍵:÷⍵
    m←⌈n÷2
    ai←∇(m,m)↑⍵
    di←∇(m,m)↓⍵
    b←(m,m-n)↑⍵
    bx←-ai+.×b+.×di
    (ai,bx)⍪((⌊n÷2),-n)↑di
  }

  0=⍴⍴⍵:÷⍵
  1=⍴⍴⍵:,∇⍪⍵
  2≠⍴⍴⍵:↗'RANK ERROR'
  0∊≥/⍴⍵:↗'LENGTH ERROR'
  (Q R)←QR ⍵
  (Rinv R)+.×+⍉Q
  ;
  (⌹⍵)+.×⍺
}
⍝ ⌹2 <=> .5
⍝ ⌹2 2⍴4 3 3 2 <=> 2 2⍴¯2 3 3 ¯4
⍝ (4 4⍴12 1 4 10 ¯6 ¯5 4 7 ¯4 9 3 4 ¯2 ¯6 7 7)⌹93 81 93.5 120.5 <=>
⍝ ... .0003898888816687221 ¯.005029566573526544 .04730651764247189 .0705568912859835
⍝ ⌹2 2 2⍴⍳8 !!! RANK ERROR
⍝ ⌹2 3⍴⍳6 !!! LENGTH ERROR

⍨←{⍵⍶⍵;⍵⍶⍺}
⍝ 17-⍨23 <=> 6
⍝ 7⍴⍨2 3 <=> 2 3⍴7
⍝ +⍨2    <=> 4
⍝ -⍨123  <=> 0

⍝ Aliases:
−←- ⍝ U+2212 MINUS SIGN        for U+002D HYPHEN-MINUS
⋆←* ⍝ U+22C6 STAR OPERATOR     for U+002A ASTERISK
∣←| ⍝ U+2223 DIVIDES           for U+007C VERTICAL LINE
^←∧ ⍝ U+005E CIRCUMFLEX ACCENT for U+2227 LOGICAL AND
∈←∊ ⍝ U+2208 ELEMENT OF        for U+220A SMALL ELEMENT OF

⍬←()
⍝ ⍬     <=> 0⍴0
⍝ ⍴⍬    <=> ,0
⍝# ⍬←5   !!!
⍝ ⍳0    <=> ⍬
⍝ ⍴0    <=> ⍬
⍝ ⍬     <=> ⍬
⍝ ⍬⍬    <=> ⍬ ⍬
⍝ 1⍬2⍬3 <=> 1 ⍬ 2 ⍬ 3

~←~⍠{(~⍺∊⍵)/⍺}
⍝ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"~"AEIOU" <=> 'BCDFGHJKLMNPQRSTVWXYZ'
⍝ 1 2 3 4 5 6~2 4 6                    <=> 1 3 5
⍝ "THIS IS TEXT"~" "                   <=> 'THISISTEXT'
⍝ "THIS" "AND" "THAT"~"T"              <=> 'THIS' 'AND' 'THAT'
⍝ "THIS" "AND" "THAT"~"AND"            <=> 'THIS' 'AND' 'THAT'
⍝ "THIS" "AND" "THAT"~⊂"AND"           <=> 'THIS' 'THAT'
⍝ "THIS" "AND" "THAT"~"TH" "AND"       <=> 'THIS' 'THAT'
⍝ 11 12 13 14 15 16~2 3⍴1 2 3 14 5 6   <=> 11 12 13 15 16
⍝ (2 2⍴⍳4)~2 !!! RANK ERROR

_hook←{⍵⍶⍹⍵;⍺⍶⍹⍵}
⍝ (+÷)\3 7 16 ¯294 <=> (3 3.142857142857143 3.1415929203539825 3.141592653921421)
⍝ (=⌊)123 <=> 1
⍝ (=⌊)123.4 <=> 0
⍝ (÷⍟)1000 <=> 144.76482730108395

⊃←{
  0=⍴⍴⍵:↑⍵
  0=×/⍴⍵:⍵
  shape←⍴⍵ ⋄ ⍵←,⍵
  r←⌈/≢¨shapes←⍴¨⍵ ⍝ maximum rank of all shapes
  max←↑⌈/shapes←(⍴ ↓ (r⍴1)∘,)¨shapes ⍝ maximum shape of rank adjusted shapes
  (shape,max)⍴↑⍪/shapes{max↑⍺⍴⍵}¨⍵
  ;
  1<⍴⍴⍺:↗'RANK ERROR'
  x←⍵
  {
    1<⍴⍴⍵:↗'RANK ERROR'
    ⍵←,⍵
    (⍴⍵)≠⍴⍴x:↗'RANK ERROR'
    ∨/⍵≥⍴x:↗'INDEX ERROR'
    x←⊃⍵⌷x
  }¨⍺
  x
}
⍝ ⊃3            <=> 3
⍝ ⊃(1 2)(3 4)   <=> 2 2⍴1 2 3 4
⍝ ⊃(1 2)(3 4 5) <=> 2 3⍴1 2 0 3 4 5
⍝ ⊃1 2          <=> 1 2
⍝ ⊃(1 2)3       <=> 2 2⍴1 2 3 0
⍝ ⊃1(2 3)       <=> 2 2⍴1 0 2 3
⍝ ⊃2 2⍴1(1 1 2⍴3 4)(5 6)(2 0⍴0) <=> 2 2 1 2 2⍴1 0 0 0 3 4 0 0 5 6 0 0 0 0 0 0
⍝ ⊃⍬            <=> ⍬
⍝ ⊃2 3 0⍴0      <=> 2 3 0⍴0
⍝ ⍬⊃3               <=> 3
⍝ 2⊃'PICK'          <=> 'C'
⍝ (⊂1 0)⊃2 2⍴'ABCD' <=> 'C'
⍝ 1⊃'foo' 'bar'     <=> 'bar'
⍝ 1 2⊃'foo' 'bar'   <=> 'r'
⍝ (2 2⍴0)⊃1 2       !!! RANK ERROR
⍝ (⊂2 1⍴0)⊃2 2⍴0    !!! RANK ERROR
⍝ (⊂2 2⍴0)⊃1 2      !!! RANK ERROR
⍝ (⊂2 2)⊃1 2        !!! RANK ERROR
⍝ (⊂0 2)⊃2 2⍴'ABCD' !!! INDEX ERROR

↓←{
  0=⍴⍴⍵:⍵ ⋄ ⊂[¯1+⍴⍴⍵]⍵
  ;
  1<⍴⍴⍺:↗'RANK ERROR'
  a←,⍺
  ⍵←{0=⍴⍴⍵:((⍴a)⍴1)⍴⍵⋄⍵}⍵
  (⍴a)>⍴⍴⍵:↗'RANK ERROR'
  a←(⍴⍴⍵)↑a
  a←((a>0)×0⌊a-⍴⍵)+(a≤0)×0⌈a+⍴⍵
  a↑⍵
}
⍝ ↓1 2 3 <=> ⊂1 2 3
⍝ ↓(1 2)(3 4) <=> ⊂(1 2)(3 4)
⍝ ↓2 2⍴⍳4 <=> (0 1)(2 3)
⍝ ↓2 3 4⍴⍳24 <=> 2 3⍴(0 1 2 3)(4 5 6 7)(8 9 10 11)(12 13 14 15)(16 17 18 19)(20 21 22 23)
⍝ 4↓'OVERBOARD'         <=> 'BOARD'
⍝ ¯5↓'OVERBOARD'        <=> 'OVER'
⍝ ⍴10↓'OVERBOARD'       <=> ,0
⍝ 0 ¯2↓3 3⍴'ONEFATFLY'  <=> 3 1⍴'OFF'
⍝ ¯2 ¯1↓3 3⍴'ONEFATFLY' <=> 1 2⍴'ON'
⍝ 1↓3 3⍴'ONEFATFLY'     <=> 2 3⍴'FATFLY'
⍝ ⍬↓3 3⍴⍳9              <=> 3 3⍴⍳9
⍝ 1 1↓2 3 4⍴"ABCDEFGHIJKLMNOPQRSTUVWXYZ"   <=> 1 2 4⍴'QRSTUVWX'
⍝ ¯1 ¯1↓2 3 4⍴"ABCDEFGHIJKLMNOPQRSTUVWXYZ" <=> 1 2 4⍴'ABCDEFGH'
⍝ 1↓0                   <=> ⍬
⍝ 0 1↓2                 <=> 1 0⍴0
⍝ 1 2↓3                 <=> 0 0⍴0
⍝ ⍬↓0                   <=> 0

⍪←{(≢⍵)(×/1↓⍴⍵)⍴⍵; ⍺,[0]⍵}
⍝ ⍪2 3 4 <=> 3 1⍴2 3 4
⍝ ⍪0 <=> 1 1⍴0
⍝ ⍪2 2⍴2 3 4 5 <=> 2 2⍴2 3 4 5
⍝ ⍴⍪2 3⍴⍳6 <=> 2 3
⍝ ⍴⍪2 3 4⍴⍳24 <=> 2 12
⍝ (2 3⍴⍳6)⍪9 <=> 3 3⍴(0 1 2 3 4 5 9 9 9)
⍝ 1⍪2 <=> 1 2

⊢←{⍵}
⍝ 123⊢456 <=> 456
⍝ ⊢456 <=> 456

⊣←{;⍺}
⍝ 123⊣456 <=> 123
⍝ ⊣456 <=> ⍬

≢←{⍬⍴(⍴⍵),1; ~⍺≡⍵}
⍝ ≢0 <=> 1
⍝ ≢0 0 0 <=> 3
⍝ ≢⍬ <=> 0
⍝ ≢2 3⍴⍳6 <=> 2
⍝ 3≢3 <=> 0

,←{(×/⍴⍵)⍴⍵}⍠,
⍝ ,2 3 4⍴'abcdefghijklmnopqrstuvwx' <=> 'abcdefghijklmnopqrstuvwx'
⍝ ,123 <=> 1⍴123

⌹←{
  norm←{(⍵+.×+⍵)*0.5}

  QR←{ ⍝ QR decomposition
    n←(⍴⍵)[1]
    1≥n:{t←norm,⍵ ⋄ (⍵÷t)(⍪t)}⍵
    m←⌈n÷2
    a0←((1↑⍴⍵),m)↑⍵
    a1←(0,m)↓⍵
    (q0 r0)←∇a0
    c←(+⍉q0)+.×a1
    (q1 r1)←∇a1-q0+.×c
    (q0,q1)((r0,c)⍪((⌊n÷2),-n)↑r1)
  }

  Rinv←{ ⍝ Inverse of an upper triangular matrix
    1=n←1↑⍴⍵:÷⍵
    m←⌈n÷2
    ai←∇(m,m)↑⍵
    di←∇(m,m)↓⍵
    b←(m,m-n)↑⍵
    bx←-ai+.×b+.×di
    (ai,bx)⍪((⌊n÷2),-n)↑di
  }

  0=⍴⍴⍵:÷⍵
  1=⍴⍴⍵:,∇⍪⍵
  2≠⍴⍴⍵:↗'RANK ERROR'
  0∊≥/⍴⍵:↗'LENGTH ERROR'
  (Q R)←QR ⍵
  (Rinv R)+.×+⍉Q
  ;
  (⌹⍵)+.×⍺
}
⍝ ⌹2 <=> .5
⍝ ⌹2 2⍴4 3 3 2 <=> 2 2⍴¯2 3 3 ¯4
⍝ (4 4⍴12 1 4 10 ¯6 ¯5 4 7 ¯4 9 3 4 ¯2 ¯6 7 7)⌹93 81 93.5 120.5 <=>
⍝ ... .0003898888816687221 ¯.005029566573526544 .04730651764247189 .0705568912859835
⍝ ⌹2 2 2⍴⍳8 !!! RANK ERROR
⍝ ⌹2 3⍴⍳6 !!! LENGTH ERROR

⍨←{⍵⍶⍵;⍵⍶⍺}
⍝ 17-⍨23 <=> 6
⍝ 7⍴⍨2 3 <=> 2 3⍴7
⍝ +⍨2    <=> 4
⍝ -⍨123  <=> 0

⍝ Aliases:
−←- ⍝ U+2212 MINUS SIGN        for U+002D HYPHEN-MINUS
⋆←* ⍝ U+22C6 STAR OPERATOR     for U+002A ASTERISK
∣←| ⍝ U+2223 DIVIDES           for U+007C VERTICAL LINE
^←∧ ⍝ U+005E CIRCUMFLEX ACCENT for U+2227 LOGICAL AND
∈←∊ ⍝ U+2208 ELEMENT OF        for U+220A SMALL ELEMENT OF


Better handling of multipart/form-data in HTTPRequest

:Class HTTPRequest

    (⎕IO ⎕ML)←1 1
    (CR LF)←NL←⎕UCS 13 10

⍝ Common Status Codes
    SC←(200 'OK')(201 'Created')(204 'No Content')(301 'Moved Permanently')(302 'Found')(303 'See Other')(304 'Not Modified')(305 'Use Proxy')(307 'Temporary Redirect')
    SC,←(400 'Bad Request')(401 'Unauthorized')(403 'Forbidden')(404 'Not Found')(405 'Method Not Allowed')(406 'Not Acceptable')(408 'Request Timeout')(409 'Conflict')
    SC,←(410 'Gone')(411 'Length Required')(412 'Precondition Failed')(413 'Request Entity Too Large')(414 'Request-URI Too Long')(415 'Unsupported Media Type')
    SC,←(500 'Internal Server Error')(501 'Not Implemented')(503 'Service Unavailable')
    SC←↑SC

⍝ Fields related to the Request

    :Field Public Instance Complete←0        ⍝ do we have a complete request?
    :Field Public Instance Input←''
    :Field Public Instance Headers←0 2⍴⊂''   ⍝ HTTPRequest header fields (plus any supplied from HTTPTrailer event)
    :Field Public Instance Method←''         ⍝ HTTP method (GET, POST, PUT, etc)
    :Field Public Instance Page←''           ⍝ Requested URI
    :Field Public Instance Body←''           ⍝ body of the request
    :Field Public Instance Host←''           ⍝ host header field
    :Field Public Instance Filename←''       ⍝ upload file name
    :Field Public Instance RESTfulReq←0      ⍝ RESTful Request (set to charvec if request is RESTful)
    :Field Public Instance Arguments←0 2⍴⊂'' ⍝ arguments provided in the URI
    :Field Public Instance PeerAddr←''       ⍝ client IP address
    :Field Public Instance PeerCert←0 0⍴⊂''  ⍝ client certificate
    :Field Public Instance Data←0 2⍴⊂''      ⍝ form data
    :Field Public Instance HTTPVersion←''
    :Field Public Instance Cookies←0 2⍴⊂''
    :Field Public Instance Session←''
    :Field Public Instance Server←⎕NS ''
    :Field Public Instance CloseConnection←0

⍝ Fields related to the Response

    :Field Public Instance Response

    GetFromTableCS←{{0∊⍴⍵:'' ⋄ 1=⍴⍵:⊃⍵ ⋄ ⍵}⍵[;2]/⍨⍵[;1]∊⊂⍺} ⍝ Case Sensitive
    GetFromTable←{(#.Strings.lc ⍺)GetFromTableCS ⍵}
    GetFromTableDefault←{⍺←'' ⋄ ⍺{0∊⍴⍵:⍺ ⋄ ⍵}⍵ GetFromTable ⍺⍺} ⍝ default_value (table ∇) value

    ine←{0∊⍴⍺:'' ⋄ ⍵} ⍝ if not empty
    inf←{∨/⍵⍷⍺:'' ⋄ ⍵} ⍝ if not found
    begins←{⍺≡(⍴⍺)↑⍵}
    split←{p←(⍺⍷⍵)⍳1 ⋄ ((p-1)↑⍵)(p↓⍵)} ⍝ Split ⍵ on first occurrence of ⍺

    ∇ r←eis w
      :Access public shared
      r←{(,∘⊂)⍣((326∊⎕DR ⍵)<2>|≡⍵),⍵}w ⍝ Enclose if simple
    ∇

    ∇ Make args;query;cookies
      :Access Public Instance
      :Implements Constructor
    ⍝ args [1] HTTP method [2] URI [3] HTTP version [4] 2-column headers

      (Method Input HTTPVersion Headers)←args
      Headers[;1]←#.Strings.lc Headers[;1]  ⍝ header names are case insensitive
      Method←#.Strings.lc Method

      Response←⎕NS''
      Response.(Status StatusText Headers File HTML HTMLHead PeerAddr NoWrap Bytes MSec)←200 'OK'(0 2⍴⊂'')0 '' '' '' 0(0 0)(⎕AI[3])

      Host←GetHeader'host'
      Page query←'?'split Input
      Page←PercentDecode Page

      :If '/'≠⊃Page  ⍝!!! need to update this to deal with absolute URI's, see https://tools.ietf.org/html/rfc7230#section-5.3.2
          1 Fail 400
          →0
      :EndIf

      Arguments←URLDecodeArgs query

      :If ~0∊⍴cookies←GetHeader'cookie'
          cookies←CookieSplit cookies
          :If ~0∊⍴cookies←(2=⊃∘⍴¨cookies)/cookies
              Cookies←↑{(' '~⍨1⊃⍵)(DeCode 2⊃⍵)}¨cookies
          :EndIf
      :EndIf
      :If 'get'≡Method
      :OrIf ∧/0∘∊∘⍴¨GetHeader¨'content-length' 'transfer-encoding'
          FinalizeRequest
      :EndIf
    ∇

    ∇ ProcessBody args
      :Access public
      Body←args
      FinalizeRequest
    ∇

    ∇ ProcessChunk args
      :Access public
     ⍝ args is [1] chunk content [2] chunk-extension name/value pairs (which we don't expect and won't process)
      Body,←1⊃args
    ∇

    ∇ ProcessTrailer args;inds;mask
      :Access public
      args[;1]←#.Strings.lc args[;1]
      mask←(≢Headers)≥inds←Headers[;1]⍳args[;1]
      Headers[mask/inds;2]←mask/args[;2]
      Headers⍪←(~mask)⌿args
      FinalizeRequest
    ∇

    ∇ FinalizeRequest;contentType;boundary;mask;new;s
      :If ~0∊⍴contentType←GetHeader'content-type'
      :AndIf 'multipart/form-data'begins contentType
          boundary←'--',(8+('boundary='⍷contentType)⍳1)↓contentType ⍝ boundary string
          Data←↑DecodeMultiPart¨¯1↓boundary{(⍴⍺)↓¨(⍺⍷⍵)⊂⍵}Body ⍝ ¯1↓ because last boundary has '--' appended
      :ElseIf 'application/x-www-form-urlencoded'begins contentType
          Data←URLDecodeArgs Body
      :EndIf
      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_serialized_' ⍝ do we have any serialized form data from AJAX?
          new←0 2⍴⊂''
          :For s :In mask/Data[;2]
              new⍪←URLDecodeArgs s
          :EndFor
          Data←((~(⊃⍴Data)↑mask)⌿Data)⍪new
          Data←{0 1∊⍨⊃⍴⍵:⊃⍵ ⋄ ⍵}¨⊃{⍺ ⍵}⌸/↓[1]Data
      :EndIf

      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_json_' ⍝ do we have any Syncfusion model data?
          :For s :In mask/⍳⍴mask
              Data[s;1]↓⍨←6
              Data[s;2]←eis #.JSON.toAPL⊃Data[s;2]
          :EndFor
      :EndIf
      Complete←1
      CloseConnection←'close'≡GetHeader'connection'
    ∇

    ∇ Wipe
      :Access public
    ⍝ clear out all request data
      Input←''
      Headers←''
      Method←''
      Page←''
      Filename←''
      RESTfulReq←0
      Arguments←''
      PeerAddr←''
      PeerCert←⍬
      Data←⍬
      Cookies←''
      Session←''
      Server←⎕NS''
      Response←⎕NS''
    ∇

    :Section Argument and Data Handling

    ∇ r←PercentDecode r;rgx;rgxu;i;j;z;t;m;⎕IO;lens;fill
      :Access public shared
    ⍝ Decode a Percent Encoded string https://en.wikipedia.org/wiki/Percent-encoding
      ⎕IO←0
      ((r='+')/r)←' '
      rgx←'[0-9a-fA-F]'
      rgxu←'%[uU]',(4×⍴rgx)⍴rgx ⍝ 4 characters
      r←(rgxu ⎕R{{⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳⍵}2↓⍵.Match})r
      :If 0≠⍴i←(r='%')/⍳⍴r
      :AndIf 0≠⍴i←(i≤¯2+⍴r)/i
          z←r[j←i∘.+1 2]
          t←'UTF-8'⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳z
          lens←⊃∘⍴¨'UTF-8'∘⎕UCS¨t  ⍝ UTF-8 is variable length encoding
          fill←i[¯1↓+\0,lens]
          r[fill]←t
          m←(⍴r)⍴1 ⋄ m[(,j),i~fill]←0
          r←m/r
      :EndIf
    ∇

    ∇ r←GetArgument name
      :Access Public Instance
      r←name GetFromTable Arguments
    ∇

    ∇ r←GetData name
      :Access Public Instance
      r←name GetFromTableCS Data
    ∇

    ∇ r←GetHeader name
      :Access Public Instance
      r←(#.Strings.lc name)GetFromTable Headers
    ∇

    ∇ r←{cs}URLDecodeArgs args
      :Access Public Shared
    ⍝ Decode arguments passed in the URI
      cs←{6::⍵ ⋄ cs}1 ⍝ default to case sensitive
      r←(args∨.≠' ')⌿↑'='∘split¨{1↓¨(⍵='&')⊂⍵}'&',args ⍝ Cut on '&'
      r[;1]←{⍵↓⍨¯6×'%5B%5D'≡¯6↑⍵}¨r[;1] ⍝ remove [] from array args
      r[;2]←PercentDecode¨r[;2]
      :If ~cs ⋄ r[;1]←#.Strings.lc¨r[;1] ⋄ :EndIf
    ∇

    ∇ r←URLEncode data;⎕IO;z;ok;nul;m
      :Access Public Shared
      nul←⎕UCS ⎕IO←0
      ok←nul,∊⎕UCS¨(⎕UCS'aA0')+⍳¨26 26 10

      z←⎕UCS'UTF-8'⎕UCS∊nul,¨,data
      :If ∨/m←~z∊ok
          (m/z)←↓'%',(⎕D,⎕A)[⍉16 16⊤⎕UCS m/z]
          data←(⍴data)⍴1↓¨{(⍵=nul)⊂⍵}∊z
      :EndIf

      r←¯1↓∊data,¨(⍴data)⍴'=&'
    ∇

    ∇ r←DecodeHeader buf;len;d;i
    ⍝ Decode HTML Header
      :Access public shared
      len←(¯1+⍴NL,NL)+⊃{((NL,NL)⍷⍵)/⍳⍴⍵}buf
      :If len>0
          d←(⍴NL)↓¨{(NL⍷⍵)⊂⍵}NL,len↑buf
          d←↑{((p-1)↑⍵)({(' '=1↑⍵)↓⍵}(p←⍵⍳':')↓⍵)}¨d
          d[i;1]←#.Strings.lc¨d[i←1↓⍳1↑⍴d;1]
      :Else
          d←⍬
      :EndIf
      r←len d
    ∇

    ∇ r←DecodeMultiPart data;ind;hdr;d;name;filename;i;upload
      hdr←data↑⍨ind←1+1⍳⍨(NL,NL)⍷data
      d←#.Strings.dlb'Content-Disposition:'GetParam hdr

      name←filename←''
      :If (⍴d)≥i←5+('name="'⍷d)⍳1
          name←(¯1+name⍳'"')↑name←i↓d
      :EndIf

      :If upload←(⍴d)≥i←9+('filename="'⍷d)⍳1
          filename←(¯1+filename⍳'"')↑filename←i↓d
      :EndIf

      data←(2+ind)↓data ⍝ Drop up to 1st doubleCR
      data←(¯1+¯1↑{⍵/⍳⍴⍵}NL⍷data)↑data ⍝ Drop from last CR

      :If upload>0∊⍴filename
          r←name(filename data)
      :Else
          r←name ('UTF-8' ⎕UCS ⎕UCS data)
      :EndIf
    ∇

    ∇ r←header GetValue(name type);i;h
      ⍝ Extract value from HTTP Header structure returned by DecodeHeader

      :If (1↑⍴header)<i←header[;1]⍳⊂lc name
          r←⍬ ⍝ Not found
      :Else
          r←⊃header[i;2]
          :If 'Numeric'≡type
              r←1⊃2⊃⎕VFI r
          :EndIf
      :EndIf
    ∇

    ∇ r←name GetParam HTTP;i;z;find
      find←{⊃(⍺⍷⍵)/⍳⍴⍵}
      r←''
      :If 0<i←name find HTTP
      :AndIf 0<i←NL find z←(¯1+i+⍴name)↓HTTP
          r←(i-1)↑z
      :EndIf
    ∇

    :endsection

    :section Cookie Functions

    ∇ DelCookie ctl;name;path
      :Access Public Instance ⍝ Delete a cookie
      ctl←eis ctl
      name path←ctl,(⍴ctl)↓'CookieName' '/'
      SetCookie name''path ¯30 ⍝ Expiry 30 days ago
    ∇

    ∇ r←GetCookie name
      :Access Public Instance
      r←name GetFromTableCS Cookies ⍝ cookie names are case sensitive
    ∇

    ∇ SetCookie ctl;name;value;path;date;z;keep;other
      :Access Public Instance ⍝ Set a Cookie
      ⍝  ctl [1] cookie name, [2] value, [3] path that cookie applies to,
      ⍝      [4] expires - can be a single number of days to offset from today
      ⍝                    a character vector of a properly formatted timestamp (see #.Dates.CookieFmt)
      ⍝                    an integer vector of 3-6 elements in ⎕TS format
      ⍝      [5] a character vector of any other cookie parameters - e.g. HttpOnly
      ctl←eis ctl
      name value path keep other←ctl,(⍴ctl)↓'CookieName' 'CookieValue' '/' 30 ''
      :If ~0∊⍴date←keep
          :If 2|⎕DR keep
              :If 1=⍴,keep
                  date←#.Dates.IDNToDate keep+#.Dates.DateToIDN ⎕TS ⍝ keep is # of days cookie should be valid
              :EndIf
              date←#.Dates.CookieFmt 6↑date
          :EndIf
      :EndIf
      z←name,'=',(⍕value),(path ine'; Path=',path),(date ine'; Expires=',date),other ine('; '≡2↑other)↓'; ',other
      Response.Headers⍪←'Set-Cookie'z
    ∇

    :endsection

    :section Request/Response Content Handling

    ∇ r←HttpStatus code
      :Access public shared
      r←code((SC[;1]⍳code)⊃SC[;2],⊂'')
    ∇

    ∇ {flag}Fail x;i;root;f;t
      :Access Public Instance
      :If 0=⎕NC'flag' ⋄ flag←0 ⋄ :EndIf
      :If 3=10|⎕DR x ⍝ Just a status code
          Response.Status←x
          :If (1↑⍴SC[;1])≥i←SC[;1]⍳x
              Response.StatusText←⊃SC[i;2] ⍝ Default text
          :EndIf
      :Else
          Response.(Status StatusText)←x
      :EndIf
      :If flag<0≡RESTfulReq
          :For root :In Server.Config.(Root MSRoot) ⍝ try site root, then server root
              :If #.Files.Exists f←root,'CommonPages\',(⍕x),'.htm'
                  :If ~0∊⍴(⎕UCS 13 10)~⍨t←#.Files.ReadText f
                      Response.HTML,⍨←t,'<br/>'
                      Response.Headers⍪←'content-type' 'text/html; charset=utf-8'
                  :EndIf
                  :Leave
              :EndIf
          :EndFor
      :EndIf
    ∇

    ∇ {code}Redirect location
      :Access public
      :If 0=⎕NC'code' ⋄ code←301 ⋄ :EndIf ⍝ default to permanent redirection
      Response.(Status StatusText)←code((SC[;1]⍳code)⊃SC[;2],⊂'')
      'Location'SetHeader location
    ∇

    ∇ r←isDesktop;cis;desktop;mobile;bot;user_agent ⍝ Detect if we think this is a desktop platform
⍝ Rationale: mobile device detection is a messy subject, therefore it's easier and perhaps safer to detect
⍝            desktop platforms (or bots) and assume that anything else is a mobile platform

      :Access public instance
      cis←{~0∊⍴(⍺ ⎕S 0 ⎕OPT'IC' 1)⍵} ⍝ Case Insensitive Search

      r←0

⍝ Some mobile browsers which look like desktop browsers.
      mobile←'(iphone|ipod|ipad|blackberry|android|palm|windows\s+ce)'
      desktop←'(windows|linux|os\s+[x9]|solaris|bsd)'
      bot←'(spider|crawl|slurp|bot)'

      →0⍴⍨0∊⍴user_agent←GetUserAgent
      →0⍴⍨mobile cis user_agent ⍝ Anything that looks like a phone isn't a desktop.
      →0⍴⍨r←desktop cis user_agent ⍝ Anything that looks like a desktop probably is.
      r←bot cis user_agent ⍝ Anything that looks like a bot should default to desktop.
    ∇

    ∇ r←GetUserAgent
      :Access public instance
      ⍝ Some mobile browsers put the User-Agent in a HTTP-X header
      →0↓⍨0∊⍴r←GetHeader'X-OPERAMINI-PHONE-UA'
      →0↓⍨0∊⍴r←GetHeader'X-SKYFIRE-PHONE'
      r←GetHeader'USER-AGENT'
    ∇

    ∇ r←isAPLJax
      :Access public instance
      r←'true'≡GetHeader'isAPLJax'
    ∇

    ∇ r←isPost
      :Access public instance
      r←Method≡'post'
    ∇

    ∇ r←isGet
      :Access public instance
      r←Method≡'get'
    ∇

    ∇ r←JSPlugIn file;root ⍝ Retrieve a JavaScript PlugIn
      :Access Public Instance
      root←{(1-⌊/'/\'⍳⍨⌽⍵)↓⍵}⎕WSID
      r←#.Files.ReadText root,'PlugIns\',file
    ∇

    ∇ Meta attrs ⍝ add a meta tag to the head element
      :Access public instance
      Response.HTMLHead,←'<meta ',attrs,' />'
    ∇

    ∇ {hdrs}Return x;type
      :Access Public Instance

      :If 2=⍴⍴x ⋄ :AndIf 2=¯1↑⍴x ⋄ x←URLEncode x ⋄ type←'text/plain' ⍝ 2-column matrix is name/value pairs
      :Else ⋄ type←'text/html'
      :EndIf
      Response.HTML←∊x
      :If 2=⎕NC'hdrs'
          Response.Headers⍪←hdrs
      :ElseIf ~∨/Response.Headers[;1](≡#.Strings.nocase)¨⊂'content-type'
          Response.Headers⍪←'content-type'(type,'; charset=utf-8') ⍝ set content-type if not already set
      :EndIf
    ∇

    ∇ {hdrs}ReturnFile filename
    ⍝ Queues a file to be returned to client
    ⍝ filename - physical name of the file
    ⍝ {hdrs}   - optional 2-column header name/value pairs
    ⍝ if hdrs is not supplied, we attempt to set the content type of file response based on the file extension
      :Access Public Instance
      :If #.Files.Exists filename
          Response.(HTML File)←filename 1
          :If 2=⎕NC'hdrs'
              Response.Headers⍪←hdrs
          :Else
              SetContentType filename
          :EndIf
      :Else
          Fail 404  ⍝ file not found
      :EndIf
    ∇

    ∇ SetContentType x
      :Access public instance
      'content-type'SetHeader'application/binary'(Server.Config.ContentTypes GetFromTableDefault)#.Strings.lc{(1-(⌽⍵)⍳'.')↑⍵}x
    ∇

    ∇ {hdr}SetHeaderIfNotSet value;val;mask
      :Access public instance
    ⍝ accepts value in forms
    ⍝ ('hdr1' 'val1')[('hdr2' 'val2')]
    ⍝ 'hdr1:val1' ['hdr2:val2']
    ⍝ 'hdr1' 'val1' ['hdr2' 'val2']
      :If 0=⎕NC'hdr'
          :If 3≡|≡value ⍝ pairs of strings
              val←↑value
          :ElseIf 2≡|≡value
              :If ∧/':'∊¨value
                  val←↑':'split¨value
              :Else
                  val←((0.5×⍴value),2)⍴value
              :EndIf
          :Else
              val←1 2⍴':'split value
          :EndIf
      :Else
          (hdr value)←eis¨hdr value
          val←hdr,⍪value
      :EndIf
      :If ∨/mask←~hdr(∊#.Strings.nocase)Response.Headers[;1]
          Response.Headers⍪←mask⌿val
      :EndIf
    ∇

    ∇ {hdr}SetHeader value;val;mask
      :Access public instance
    ⍝ accepts value in forms
    ⍝ ('hdr1' 'val1')[('hdr2' 'val2')]
    ⍝ 'hdr1:val1' ['hdr2:val2']
    ⍝ 'hdr1' 'val1' ['hdr2' 'val2']
      :If 0=⎕NC'hdr'
          :If 3≡|≡value ⍝ pairs of strings
              val←↑value
          :ElseIf 2≡|≡value
              :If ∧/':'∊¨value
                  val←↑':'split¨value
              :Else
                  val←((0.5×⍴value),2)⍴value
              :EndIf
          :Else
              val←1 2⍴':'split value
          :EndIf
      :Else
          (hdr value)←eis¨hdr value
          val←hdr,⍪value
      :EndIf
      Response.Headers⍪←val
    ∇

    ∇ {tags}Script x
      :Access Public Instance
      :If 0=⎕NC'tags' ⋄ tags←'type="text/javascript"'
      :ElseIf ~∨/'type='⍷tags ⋄ tags,←' type="text/javascript"'
      :EndIf
      Response.HTMLHead,←Response.HTMLHead inf'<script ',tags,'>',(x ine NL,x),'</script>',NL
    ∇

    ∇ {style}Style file ⍝ insert a style into head element
      :Access Public Instance
      :If 0=⎕NC'style'
          :If ~0∊⍴file
              Response.HTMLHead,←Response.HTMLHead inf'<link href="',file,'" rel="stylesheet" type="text/css" />',NL
          :EndIf
      :Else
          :If 1<|≡style ⋄ style←NL,∊(,∘NL)¨style ⋄ :EndIf
          Response.HTMLHead,←Response.HTMLHead inf'<style type="text/css">',style,'</style>',NL
      :EndIf
    ∇

    ∇ Title x
      :Access Public Instance
      Response.HTMLHead,←'<title>',x,'</title>',NL
    ∇

    ∇ Use x;n;ind
      :Access public instance
      :If 0≠⎕NC⊂'Server.Config.Resources'
      :AndIf ~0∊n←1↑⍴Server.Config.Resources
          :If n≥ind←Server.Config.Resources[;1]⍳⊂x
              (Script∘'')¨{'src="',⍵,'"'}¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 2)⊃Server.Config.Resources
              Style¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 3)⊃Server.Config.Resources
          :Else
              1 Server.Log Page,' references unknown resource: ',x
          :EndIf
      :EndIf
    ∇

    :endsection

      Split←{(⎕IO ⎕ML)←0 3         ⍝ Split Http syntax.
          ⍺←'?&=' ⋄ (,⍺){        ⍝ Default separator chars.
              0=⍴⍺:⍵             ⍝ No separator chars left: finished.
              sepr←↑⍺            ⍝ First char is separator char.
              (1↓⍺)∘∇∘{          ⍝ Recursively split at each sub level.
                  ⍵~sepr         ⍝ Separator removed.
              }¨(1+⍵=sepr)⊂⍵     ⍝ String partitioned at separator char.
          }⍵                     ⍝ Http string.
      }

      CookieSplit←{(⎕IO ⎕ML)←0 3   ⍝ Split cookies
          {{db←{⍵/⍨∨\⍵≠' '} ⋄ ⌽db⌽db ⍵}¨⍵⊂⍨~<\'='=⍵}¨⍵⊂⍨⍵≠';'}

      DeCode←{(⎕IO ⎕ML)←0 3          ⍝ Decode Special chars in HTML string.
          hex←'0123456789ABCDEF'     ⍝ Hex chars.
          {                          ⍝ Convert numbers.
              v f←⎕VFI ⍵             ⍝ Check for numbers.
              ~∧/v:⍵                 ⍝ Not all numbers: char vec.
              1=⍴f:↑f ⋄ f            ⍝ Numeric scalar or vector.
          }∊{                        ⍝ Enlist of segments.
              '%'≠↑⍵:⍵               ⍝ 1st seg may not contain special char.
              (⎕UCS 16⊥hex⍳1↓3↑⍵),3↓⍵  ⍝ Hex code replaced with corresp. ⎕AV char.
          }¨(1+⍵='%')⊂,⍵             ⍝ Segments split at '%' char.
      }

    ∇ Show
      :Access public
      ↑{⍵(⍎⍵)}¨'Input' 'Method' 'Page' 'Headers' 'Arguments' 'Data' 'Cookies'
    ∇

:EndClass

:Class HTTPRequest

    (⎕IO ⎕ML)←1 1
    (CR LF)←NL←⎕UCS 13 10

⍝ Common Status Codes
    SC←(200 'OK')(201 'Created')(204 'No Content')(301 'Moved Permanently')(302 'Found')(303 'See Other')(304 'Not Modified')(305 'Use Proxy')(307 'Temporary Redirect')
    SC,←(400 'Bad Request')(401 'Unauthorized')(403 'Forbidden')(404 'Not Found')(405 'Method Not Allowed')(406 'Not Acceptable')(408 'Request Timeout')(409 'Conflict')
    SC,←(410 'Gone')(411 'Length Required')(412 'Precondition Failed')(413 'Request Entity Too Large')(414 'Request-URI Too Long')(415 'Unsupported Media Type')
    SC,←(500 'Internal Server Error')(501 'Not Implemented')(503 'Service Unavailable')
    SC←↑SC

⍝ Fields related to the Request

    :Field Public Instance Complete←0        ⍝ do we have a complete request?
    :Field Public Instance Input←''
    :Field Public Instance Headers←0 2⍴⊂''   ⍝ HTTPRequest header fields (plus any supplied from HTTPTrailer event)
    :Field Public Instance Method←''         ⍝ HTTP method (GET, POST, PUT, etc)
    :Field Public Instance Page←''           ⍝ Requested URI
    :Field Public Instance Body←''           ⍝ body of the request
    :Field Public Instance Host←''           ⍝ host header field
    :Field Public Instance Filename←''       ⍝ upload file name
    :Field Public Instance RESTfulReq←0      ⍝ RESTful Request (set to charvec if request is RESTful)
    :Field Public Instance Arguments←0 2⍴⊂'' ⍝ arguments provided in the URI
    :Field Public Instance PeerAddr←''       ⍝ client IP address
    :Field Public Instance PeerCert←0 0⍴⊂''  ⍝ client certificate
    :Field Public Instance Data←0 2⍴⊂''      ⍝ form data
    :Field Public Instance HTTPVersion←''
    :Field Public Instance Cookies←0 2⍴⊂''
    :Field Public Instance Session←''
    :Field Public Instance Server←⎕NS ''
    :Field Public Instance CloseConnection←0

⍝ Fields related to the Response

    :Field Public Instance Response

    GetFromTableCS←{{0∊⍴⍵:'' ⋄ 1=⍴⍵:⊃⍵ ⋄ ⍵}⍵[;2]/⍨⍵[;1]∊⊂⍺} ⍝ Case Sensitive
    GetFromTable←{(#.Strings.lc ⍺)GetFromTableCS ⍵}
    GetFromTableDefault←{⍺←'' ⋄ ⍺{0∊⍴⍵:⍺ ⋄ ⍵}⍵ GetFromTable ⍺⍺} ⍝ default_value (table ∇) value

    ine←{0∊⍴⍺:'' ⋄ ⍵} ⍝ if not empty
    inf←{∨/⍵⍷⍺:'' ⋄ ⍵} ⍝ if not found
    begins←{⍺≡(⍴⍺)↑⍵}
    split←{p←(⍺⍷⍵)⍳1 ⋄ ((p-1)↑⍵)(p↓⍵)} ⍝ Split ⍵ on first occurrence of ⍺

    ∇ r←eis w
      :Access public shared
      r←{(,∘⊂)⍣((326∊⎕DR ⍵)<2>|≡⍵),⍵}w ⍝ Enclose if simple
    ∇

    ∇ Make args;query;cookies
      :Access Public Instance
      :Implements Constructor
    ⍝ args [1] HTTP method [2] URI [3] HTTP version [4] 2-column headers

      (Method Input HTTPVersion Headers)←args
      Headers[;1]←#.Strings.lc Headers[;1]  ⍝ header names are case insensitive
      Method←#.Strings.lc Method

      Response←⎕NS''
      Response.(Status StatusText Headers File HTML HTMLHead PeerAddr NoWrap Bytes MSec)←200 'OK'(0 2⍴⊂'')0 '' '' '' 0(0 0)(⎕AI[3])

      Host←GetHeader'host'
      Page query←'?'split Input
      Page←PercentDecode Page

      :If '/'≠⊃Page  ⍝!!! need to update this to deal with absolute URI's, see https://tools.ietf.org/html/rfc7230#section-5.3.2
          1 Fail 400
          →0
      :EndIf

      Arguments←URLDecodeArgs query

      :If ~0∊⍴cookies←GetHeader'cookie'
          cookies←CookieSplit cookies
          :If ~0∊⍴cookies←(2=⊃∘⍴¨cookies)/cookies
              Cookies←↑{(' '~⍨1⊃⍵)(DeCode 2⊃⍵)}¨cookies
          :EndIf
      :EndIf
      :If 'get'≡Method
      :OrIf ∧/0∘∊∘⍴¨GetHeader¨'content-length' 'transfer-encoding'
          FinalizeRequest
      :EndIf
    ∇

    ∇ ProcessBody args
      :Access public
      Body←args
      FinalizeRequest
    ∇

    ∇ ProcessChunk args
      :Access public
     ⍝ args is [1] chunk content [2] chunk-extension name/value pairs (which we don't expect and won't process)
      Body,←1⊃args
    ∇

    ∇ ProcessTrailer args;inds;mask
      :Access public
      args[;1]←#.Strings.lc args[;1]
      mask←(≢Headers)≥inds←Headers[;1]⍳args[;1]
      Headers[mask/inds;2]←mask/args[;2]
      Headers⍪←(~mask)⌿args
      FinalizeRequest
    ∇

    ∇ FinalizeRequest;contentType;boundary;mask;new;s
      :If ~0∊⍴contentType←GetHeader'content-type'
      :AndIf 'multipart/form-data'begins contentType
          boundary←'--',(8+('boundary='⍷contentType)⍳1)↓contentType ⍝ boundary string
          Data←↑DecodeMultiPart¨¯1↓boundary{(⍴⍺)↓¨(⍺⍷⍵)⊂⍵}Body ⍝ ¯1↓ because last boundary has '--' appended
      :ElseIf 'application/x-www-form-urlencoded'begins contentType
          Data←URLDecodeArgs Body
      :EndIf
      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_serialized_' ⍝ do we have any serialized form data from AJAX?
          new←0 2⍴⊂''
          :For s :In mask/Data[;2]
              new⍪←URLDecodeArgs s
          :EndFor
          Data←((~(⊃⍴Data)↑mask)⌿Data)⍪new
          Data←{0 1∊⍨⊃⍴⍵:⊃⍵ ⋄ ⍵}¨⊃{⍺ ⍵}⌸/↓[1]Data
      :EndIf

      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_json_' ⍝ do we have any Syncfusion model data?
          :For s :In mask/⍳⍴mask
              Data[s;1]↓⍨←6
              Data[s;2]←eis #.JSON.toAPL⊃Data[s;2]
          :EndFor
      :EndIf
      Complete←1
      CloseConnection←'close'≡GetHeader'connection'
    ∇

    ∇ Wipe
      :Access public
    ⍝ clear out all request data
      Input←''
      Headers←''
      Method←''
      Page←''
      Filename←''
      RESTfulReq←0
      Arguments←''
      PeerAddr←''
      PeerCert←⍬
      Data←⍬
      Cookies←''
      Session←''
      Server←⎕NS''
      Response←⎕NS''
    ∇

    :Section Argument and Data Handling

    ∇ r←PercentDecode r;rgx;rgxu;i;j;z;t;m;⎕IO;lens;fill
      :Access public shared
    ⍝ Decode a Percent Encoded string https://en.wikipedia.org/wiki/Percent-encoding
      ⎕IO←0
      ((r='+')/r)←' '
      rgx←'[0-9a-fA-F]'
      rgxu←'%[uU]',(4×⍴rgx)⍴rgx ⍝ 4 characters
      r←(rgxu ⎕R{{⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳⍵}2↓⍵.Match})r
      :If 0≠⍴i←(r='%')/⍳⍴r
      :AndIf 0≠⍴i←(i≤¯2+⍴r)/i
          z←r[j←i∘.+1 2]
          t←'UTF-8'⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳z
          lens←⊃∘⍴¨'UTF-8'∘⎕UCS¨t  ⍝ UTF-8 is variable length encoding
          fill←i[¯1↓+\0,lens]
          r[fill]←t
          m←(⍴r)⍴1 ⋄ m[(,j),i~fill]←0
          r←m/r
      :EndIf
    ∇

    ∇ r←GetArgument name
      :Access Public Instance
      r←name GetFromTable Arguments
    ∇

    ∇ r←GetData name
      :Access Public Instance
      r←name GetFromTableCS Data
    ∇

    ∇ r←GetHeader name
      :Access Public Instance
      r←(#.Strings.lc name)GetFromTable Headers
    ∇

    ∇ r←{cs}URLDecodeArgs args
      :Access Public Shared
    ⍝ Decode arguments passed in the URI
      cs←{6::⍵ ⋄ cs}1 ⍝ default to case sensitive
      r←(args∨.≠' ')⌿↑'='∘split¨{1↓¨(⍵='&')⊂⍵}'&',args ⍝ Cut on '&'
      r[;1]←{⍵↓⍨¯6×'%5B%5D'≡¯6↑⍵}¨r[;1] ⍝ remove [] from array args
      r[;2]←PercentDecode¨r[;2]
      :If ~cs ⋄ r[;1]←#.Strings.lc¨r[;1] ⋄ :EndIf
    ∇

    ∇ r←URLEncode data;⎕IO;z;ok;nul;m
      :Access Public Shared
      nul←⎕UCS ⎕IO←0
      ok←nul,∊⎕UCS¨(⎕UCS'aA0')+⍳¨26 26 10

      z←⎕UCS'UTF-8'⎕UCS∊nul,¨,data
      :If ∨/m←~z∊ok
          (m/z)←↓'%',(⎕D,⎕A)[⍉16 16⊤⎕UCS m/z]
          data←(⍴data)⍴1↓¨{(⍵=nul)⊂⍵}∊z
      :EndIf

      r←¯1↓∊data,¨(⍴data)⍴'=&'
    ∇

    ∇ r←DecodeHeader buf;len;d;i
    ⍝ Decode HTML Header
      :Access public shared
      len←(¯1+⍴NL,NL)+⊃{((NL,NL)⍷⍵)/⍳⍴⍵}buf
      :If len>0
          d←(⍴NL)↓¨{(NL⍷⍵)⊂⍵}NL,len↑buf
          d←↑{((p-1)↑⍵)({(' '=1↑⍵)↓⍵}(p←⍵⍳':')↓⍵)}¨d
          d[i;1]←#.Strings.lc¨d[i←1↓⍳1↑⍴d;1]
      :Else
          d←⍬
      :EndIf
      r←len d
    ∇

    ∇ r←DecodeMultiPart data;ind;hdr;d;name;filename;i;upload
      hdr←data↑⍨ind←1+1⍳⍨(NL,NL)⍷data
      d←#.Strings.dlb'Content-Disposition:'GetParam hdr

      name←filename←''
      :If (⍴d)≥i←5+('name="'⍷d)⍳1
          name←(¯1+name⍳'"')↑name←i↓d
      :EndIf

      :If upload←(⍴d)≥i←9+('filename="'⍷d)⍳1
          filename←(¯1+filename⍳'"')↑filename←i↓d
      :EndIf

      data←(2+ind)↓data ⍝ Drop up to 1st doubleCR
      data←(¯1+¯1↑{⍵/⍳⍴⍵}NL⍷data)↑data ⍝ Drop from last CR

      :If upload>0∊⍴filename
          r←name(filename data)
      :Else
          r←name data
      :EndIf
    ∇

    ∇ r←header GetValue(name type);i;h
      ⍝ Extract value from HTTP Header structure returned by DecodeHeader

      :If (1↑⍴header)<i←header[;1]⍳⊂lc name
          r←⍬ ⍝ Not found
      :Else
          r←⊃header[i;2]
          :If 'Numeric'≡type
              r←1⊃2⊃⎕VFI r
          :EndIf
      :EndIf
    ∇

    ∇ r←name GetParam HTTP;i;z;find
      find←{⊃(⍺⍷⍵)/⍳⍴⍵}
      r←''
      :If 0<i←name find HTTP
      :AndIf 0<i←NL find z←(¯1+i+⍴name)↓HTTP
          r←(i-1)↑z
      :EndIf
    ∇

    :endsection

    :section Cookie Functions

    ∇ DelCookie ctl;name;path
      :Access Public Instance ⍝ Delete a cookie
      ctl←eis ctl
      name path←ctl,(⍴ctl)↓'CookieName' '/'
      SetCookie name''path ¯30 ⍝ Expiry 30 days ago
    ∇

    ∇ r←GetCookie name
      :Access Public Instance
      r←name GetFromTableCS Cookies ⍝ cookie names are case sensitive
    ∇

    ∇ SetCookie ctl;name;value;path;date;z;keep;other
      :Access Public Instance ⍝ Set a Cookie
      ⍝  ctl [1] cookie name, [2] value, [3] path that cookie applies to,
      ⍝      [4] expires - can be a single number of days to offset from today
      ⍝                    a character vector of a properly formatted timestamp (see #.Dates.CookieFmt)
      ⍝                    an integer vector of 3-6 elements in ⎕TS format
      ⍝      [5] a character vector of any other cookie parameters - e.g. HttpOnly
      ctl←eis ctl
      name value path keep other←ctl,(⍴ctl)↓'CookieName' 'CookieValue' '/' 30 ''
      :If ~0∊⍴date←keep
          :If 2|⎕DR keep
              :If 1=⍴,keep
                  date←#.Dates.IDNToDate keep+#.Dates.DateToIDN ⎕TS ⍝ keep is # of days cookie should be valid
              :EndIf
              date←#.Dates.CookieFmt 6↑date
          :EndIf
      :EndIf
      z←name,'=',(⍕value),(path ine'; Path=',path),(date ine'; Expires=',date),other ine('; '≡2↑other)↓'; ',other
      Response.Headers⍪←'Set-Cookie'z
    ∇

    :endsection

    :section Request/Response Content Handling

    ∇ r←HttpStatus code
      :Access public shared
      r←code((SC[;1]⍳code)⊃SC[;2],⊂'')
    ∇

    ∇ {flag}Fail x;i;root;f;t
      :Access Public Instance
      :If 0=⎕NC'flag' ⋄ flag←0 ⋄ :EndIf
      :If 3=10|⎕DR x ⍝ Just a status code
          Response.Status←x
          :If (1↑⍴SC[;1])≥i←SC[;1]⍳x
              Response.StatusText←⊃SC[i;2] ⍝ Default text
          :EndIf
      :Else
          Response.(Status StatusText)←x
      :EndIf
      :If flag<0≡RESTfulReq
          :For root :In Server.Config.(Root MSRoot) ⍝ try site root, then server root
              :If #.Files.Exists f←root,'CommonPages\',(⍕x),'.htm'
                  :If ~0∊⍴(⎕UCS 13 10)~⍨t←#.Files.ReadText f
                      Response.HTML,⍨←t,'<br/>'
                      Response.Headers⍪←'content-type' 'text/html; charset=utf-8'
                  :EndIf
                  :Leave
              :EndIf
          :EndFor
      :EndIf
    ∇

    ∇ {code}Redirect location
      :Access public
      :If 0=⎕NC'code' ⋄ code←301 ⋄ :EndIf ⍝ default to permanent redirection
      Response.(Status StatusText)←code((SC[;1]⍳code)⊃SC[;2],⊂'')
      'Location'SetHeader location
    ∇

    ∇ r←isDesktop;cis;desktop;mobile;bot;user_agent ⍝ Detect if we think this is a desktop platform
⍝ Rationale: mobile device detection is a messy subject, therefore it's easier and perhaps safer to detect
⍝            desktop platforms (or bots) and assume that anything else is a mobile platform

      :Access public instance
      cis←{~0∊⍴(⍺ ⎕S 0 ⎕OPT'IC' 1)⍵} ⍝ Case Insensitive Search

      r←0

⍝ Some mobile browsers which look like desktop browsers.
      mobile←'(iphone|ipod|ipad|blackberry|android|palm|windows\s+ce)'
      desktop←'(windows|linux|os\s+[x9]|solaris|bsd)'
      bot←'(spider|crawl|slurp|bot)'

      →0⍴⍨0∊⍴user_agent←GetUserAgent
      →0⍴⍨mobile cis user_agent ⍝ Anything that looks like a phone isn't a desktop.
      →0⍴⍨r←desktop cis user_agent ⍝ Anything that looks like a desktop probably is.
      r←bot cis user_agent ⍝ Anything that looks like a bot should default to desktop.
    ∇

    ∇ r←GetUserAgent
      :Access public instance
      ⍝ Some mobile browsers put the User-Agent in a HTTP-X header
      →0↓⍨0∊⍴r←GetHeader'X-OPERAMINI-PHONE-UA'
      →0↓⍨0∊⍴r←GetHeader'X-SKYFIRE-PHONE'
      r←GetHeader'USER-AGENT'
    ∇

    ∇ r←isAPLJax
      :Access public instance
      r←'true'≡GetHeader'isAPLJax'
    ∇

    ∇ r←isPost
      :Access public instance
      r←Method≡'post'
    ∇

    ∇ r←isGet
      :Access public instance
      r←Method≡'get'
    ∇

    ∇ r←JSPlugIn file;root ⍝ Retrieve a JavaScript PlugIn
      :Access Public Instance
      root←{(1-⌊/'/\'⍳⍨⌽⍵)↓⍵}⎕WSID
      r←#.Files.ReadText root,'PlugIns\',file
    ∇

    ∇ Meta attrs ⍝ add a meta tag to the head element
      :Access public instance
      Response.HTMLHead,←'<meta ',attrs,' />'
    ∇

    ∇ {hdrs}Return x;type
      :Access Public Instance

      :If 2=⍴⍴x ⋄ :AndIf 2=¯1↑⍴x ⋄ x←URLEncode x ⋄ type←'text/plain' ⍝ 2-column matrix is name/value pairs
      :Else ⋄ type←'text/html'
      :EndIf
      Response.HTML←∊x
      :If 2=⎕NC'hdrs'
          Response.Headers⍪←hdrs
      :ElseIf ~∨/Response.Headers[;1](≡#.Strings.nocase)¨⊂'content-type'
          Response.Headers⍪←'content-type'(type,'; charset=utf-8') ⍝ set content-type if not already set
      :EndIf
    ∇

    ∇ {hdrs}ReturnFile filename
    ⍝ Queues a file to be returned to client
    ⍝ filename - physical name of the file
    ⍝ {hdrs}   - optional 2-column header name/value pairs
    ⍝ if hdrs is not supplied, we attempt to set the content type of file response based on the file extension
      :Access Public Instance
      :If #.Files.Exists filename
          Response.(HTML File)←filename 1
          :If 2=⎕NC'hdrs'
              Response.Headers⍪←hdrs
          :Else
              SetContentType filename
          :EndIf
      :Else
          Fail 404  ⍝ file not found
      :EndIf
    ∇

    ∇ SetContentType x
      :Access public instance
      'content-type'SetHeader'application/binary'(Server.Config.ContentTypes GetFromTableDefault)#.Strings.lc{(1-(⌽⍵)⍳'.')↑⍵}x
    ∇

    ∇ {hdr}SetHeaderIfNotSet value;val;mask
      :Access public instance
    ⍝ accepts value in forms
    ⍝ ('hdr1' 'val1')[('hdr2' 'val2')]
    ⍝ 'hdr1:val1' ['hdr2:val2']
    ⍝ 'hdr1' 'val1' ['hdr2' 'val2']
      :If 0=⎕NC'hdr'
          :If 3≡|≡value ⍝ pairs of strings
              val←↑value
          :ElseIf 2≡|≡value
              :If ∧/':'∊¨value
                  val←↑':'split¨value
              :Else
                  val←((0.5×⍴value),2)⍴value
              :EndIf
          :Else
              val←1 2⍴':'split value
          :EndIf
      :Else
          (hdr value)←eis¨hdr value
          val←hdr,⍪value
      :EndIf
      :If ∨/mask←~hdr(∊#.Strings.nocase)Response.Headers[;1]
          Response.Headers⍪←mask⌿val
      :EndIf
    ∇

    ∇ {hdr}SetHeader value;val;mask
      :Access public instance
    ⍝ accepts value in forms
    ⍝ ('hdr1' 'val1')[('hdr2' 'val2')]
    ⍝ 'hdr1:val1' ['hdr2:val2']
    ⍝ 'hdr1' 'val1' ['hdr2' 'val2']
      :If 0=⎕NC'hdr'
          :If 3≡|≡value ⍝ pairs of strings
              val←↑value
          :ElseIf 2≡|≡value
              :If ∧/':'∊¨value
                  val←↑':'split¨value
              :Else
                  val←((0.5×⍴value),2)⍴value
              :EndIf
          :Else
              val←1 2⍴':'split value
          :EndIf
      :Else
          (hdr value)←eis¨hdr value
          val←hdr,⍪value
      :EndIf
      Response.Headers⍪←val
    ∇

    ∇ {tags}Script x
      :Access Public Instance
      :If 0=⎕NC'tags' ⋄ tags←'type="text/javascript"'
      :ElseIf ~∨/'type='⍷tags ⋄ tags,←' type="text/javascript"'
      :EndIf
      Response.HTMLHead,←Response.HTMLHead inf'<script ',tags,'>',(x ine NL,x),'</script>',NL
    ∇

    ∇ {style}Style file ⍝ insert a style into head element
      :Access Public Instance
      :If 0=⎕NC'style'
          :If ~0∊⍴file
              Response.HTMLHead,←Response.HTMLHead inf'<link href="',file,'" rel="stylesheet" type="text/css" />',NL
          :EndIf
      :Else
          :If 1<|≡style ⋄ style←NL,∊(,∘NL)¨style ⋄ :EndIf
          Response.HTMLHead,←Response.HTMLHead inf'<style type="text/css">',style,'</style>',NL
      :EndIf
    ∇

    ∇ Title x
      :Access Public Instance
      Response.HTMLHead,←'<title>',x,'</title>',NL
    ∇

    ∇ Use x;n;ind
      :Access public instance
      :If 0≠⎕NC⊂'Server.Config.Resources'
      :AndIf ~0∊n←1↑⍴Server.Config.Resources
          :If n≥ind←Server.Config.Resources[;1]⍳⊂x
              (Script∘'')¨{'src="',⍵,'"'}¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 2)⊃Server.Config.Resources
              Style¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 3)⊃Server.Config.Resources
          :Else
              1 Server.Log Page,' references unknown resource: ',x
          :EndIf
      :EndIf
    ∇

    :endsection

      Split←{(⎕IO ⎕ML)←0 3         ⍝ Split Http syntax.
          ⍺←'?&=' ⋄ (,⍺){        ⍝ Default separator chars.
              0=⍴⍺:⍵             ⍝ No separator chars left: finished.
              sepr←↑⍺            ⍝ First char is separator char.
              (1↓⍺)∘∇∘{          ⍝ Recursively split at each sub level.
                  ⍵~sepr         ⍝ Separator removed.
              }¨(1+⍵=sepr)⊂⍵     ⍝ String partitioned at separator char.
          }⍵                     ⍝ Http string.
      }

      CookieSplit←{(⎕IO ⎕ML)←0 3   ⍝ Split cookies
          {{db←{⍵/⍨∨\⍵≠' '} ⋄ ⌽db⌽db ⍵}¨⍵⊂⍨~<\'='=⍵}¨⍵⊂⍨⍵≠';'}

      DeCode←{(⎕IO ⎕ML)←0 3          ⍝ Decode Special chars in HTML string.
          hex←'0123456789ABCDEF'     ⍝ Hex chars.
          {                          ⍝ Convert numbers.
              v f←⎕VFI ⍵             ⍝ Check for numbers.
              ~∧/v:⍵                 ⍝ Not all numbers: char vec.
              1=⍴f:↑f ⋄ f            ⍝ Numeric scalar or vector.
          }∊{                        ⍝ Enlist of segments.
              '%'≠↑⍵:⍵               ⍝ 1st seg may not contain special char.
              (⎕UCS 16⊥hex⍳1↓3↑⍵),3↓⍵  ⍝ Hex code replaced with corresp. ⎕AV char.
          }¨(1+⍵='%')⊂,⍵             ⍝ Segments split at '%' char.
      }

    ∇ Show
      :Access public
      ↑{⍵(⍎⍵)}¨'Input' 'Method' 'Page' 'Headers' 'Arguments' 'Data' 'Cookies'
    ∇

:EndClass


More MacOS file changes

More MacOS file changes

:Namespace Files

⍝ Provides cover functions for many file operations
⍝ Eventually all will be implemented for both Windows and *nix

    (⎕IO ⎕ML)←1

    ∇ r←text AppendText name;tn
     ⍝ Append text to existing file (must be single byte text)
      tn←name ⎕NTIE 0
      r←text ⎕NAPPEND tn(⎕DR' ')
      ⎕NUNTIE tn
    ∇

    ∇ (tn name)←{ext}CreateTemp folder;cnt
     ⍝ Create a temp file
     ⍝ folder - folder to create temp file in
     ⍝ ext - file extension
      :If 0=⎕NC'ext' ⋄ ext←'tmp' ⋄ :EndIf
      :If 0∊⍴ext ⋄ ext←'tmp' ⋄ :EndIf
      cnt←tn←0
      :Repeat
          name←folder,('/\'∊⍨¯1↑folder)↓'/',⎕A[?8⍴⍴⎕A],'.',ext
          :Trap 22 ⋄ tn←name ⎕NCREATE 0 ⋄ :EndTrap
          cnt+←1
      :Until (cnt>20)∨tn≠0
      'Unable to create temporary file'⎕SIGNAL tn↓22
    ∇

    ∇ {protect}Copy FmTo;CopyFileX;GetLastError ⍝ Copy file Fm -> To
      :Select APLVersion
      :Case '*nix'
          ∘
      :Case 'Win'
          :If 0=⎕NC'protect'     ⍝ Copy fails if <protect> and 'To' exists.
              protect←0          ⍝ Default unprotected copy.
          :EndIf
          'CopyFileX'⎕NA'I kernel32.C32∣CopyFile* <0T <0T I'
          :If 0=CopyFileX FmTo,protect
              ⎕NA'I4 kernel32.C32|GetLastError'
              11 ⎕SIGNAL⍨'CopyFile error:',⍕GetLastError
          :EndIf
      :EndSelect
    ∇

    ∇ Delete name;DeleteFileX;GetLastError;FindFirstFile;FindNextFile;FindClose;handle;rslt;ok;next;⎕IO;path
      ⎕IO←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          _SH'rm ',unixfix name
      :Case 'Win'
          'DeleteFileX'⎕NA'I kernel32.C32∣DeleteFile* <0T'
          ⎕NA'I4 kernel32.C32|GetLastError'
          :If ∨/'*?'∊name ⍝ wildcards?
              path←{(⌽∨\⌽⍵∊'\/')/⍵}name
              _FindDefine
              handle rslt←_FindFirstFile name
              :If 0=handle
                  :Return ⍝ ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
              :EndIf
              :If '.'≠⊃6⊃rslt
                  {}DeleteFileX⊂path,6⊃rslt
              :EndIf
              :While 1=0⊃ok next←_FindNextFile handle
                  :If '.'≠⊃6⊃next
                      {}DeleteFileX⊂path,6⊃next
                  :EndIf
              :EndWhile
              :If 0 18∨.≠ok next
                  ('ntdir error:',⍕next)⎕SIGNAL 11   ⍝ DOMAIN
              :EndIf
              {}FindClose handle
          :Else
              :If 0=DeleteFileX⊂name
                  11 ⎕SIGNAL⍨'DeleteFile error:',⍕GetLastError
              :EndIf
          :EndIf
      :EndSelect
    ∇

    ∇ rslt←{amsk}Dir path;handle;next;ok;⎕IO;attrs;FindFirstFileX;FindNextFileX;FindClose;FileTimeToLocalFileTime;FileTimeToSystemTime;GetLastError
     ⍝ 'FIX ME'⎕SIGNAL 11
     ⍝ Amsk is a 32 element bool attribute mask.
     ⍝ Only files with attributes corresponding to 1-s in the mask will be returned.
     ⍝ '*'s mark default attribute mask.
     ⍝
     ⍝        * [31] <=> READONLY
     ⍝          [30] <=> HIDDEN
     ⍝        * [29] <=> SYSTEM
     ⍝          [28] <=> undocumented
     ⍝        * [27] <=> DIRECTORY
     ⍝        * [26] <=> ARCHIVE
     ⍝          [25] <=> DEVICE
     ⍝        * [24] <=> NORMAL - only set if no other bits are set
     ⍝        * [23] <=> TEMPORARY
     ⍝        * [22] <=> SPARSE FILE
     ⍝        * [21] <=> REPARSE POINT
     ⍝        * [20] <=> COMPRESSED
     ⍝        * [19] <=> OFFLINE
     ⍝        * [18] <=> NOT CONTENT INDEXED
     ⍝        * [17] <=> ENCRYPTED
     ⍝        * rest <=> undocumented (but in the default set so that
     ⍝                   Microsoft can extend them)
    ⍝ rslt is a vector of character vectors of filenames

      ⎕IO←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          :If 0=⎕NC'amsk'
              amsk←~(⍳32)∊30 28 25   ⍝ Default attribute mask.
          :EndIf
          _FindDefine
          handle rslt←_FindFirstFile path
          :If 0=handle
              ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
          :EndIf
          rslt←,⊂rslt
          :While 1=0⊃ok next←_FindNextFile handle
              rslt,←⊂next
          :EndWhile
          :If 0 18∨.≠ok next
              ('ntdir error:',⍕next)⎕SIGNAL 11       ⍝ DOMAIN
          :EndIf
          ok←FindClose handle
          rslt←↓[0]↑rslt
          attrs←(32⍴2)⊤0⊃rslt                        ⍝ Get attributes into bits
          rslt←(amsk∧.≥attrs)⌿6⊃rslt                 ⍝ bin unwanted files and info
      :EndSelect
    ∇

    ∇ r←DirExists path
      r←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          :Trap 11
              r←0<⍴_SH'ls -adl ',unixfix path
          :EndTrap
      :Case 'Win'
          r←0<⍬⍴⍴'.'List path
      :EndSelect
    ∇

    ∇ rslt←{amsk}DirX path;handle;next;ok;attrs;⎕IO;FindFirstFileX;FindNextFileX;FindClose;FileTimeToLocalFileTime;FileTimeToSystemTime;GetLastError
     ⍝ 'FIX ME'⎕SIGNAL 11
     ⍝ Amsk is a 32 element bool attribute mask.
     ⍝ Only files with attributes corresponding to 1-s in the mask will be returned.
     ⍝ Amsk defaults to all attributes.
     ⍝ 0⊃rslt <=> 32 column boolean matrix of attribute bits
     ⍝          [;31] <=> READONLY
     ⍝          [;30] <=> HIDDEN
     ⍝          [;29] <=> SYSTEM
     ⍝          [;28] <=> undocumented
     ⍝          [;27] <=> DIRECTORY
     ⍝          [;26] <=> ARCHIVE
     ⍝          [;25] <=> undocumented
     ⍝          [;24] <=> NORMAL - only set if no other bits are set
     ⍝          [;23] <=> TEMPORARY
     ⍝          [;22] <=> SPARSE FILE
     ⍝          [;21] <=> REPARSE POINT
     ⍝          [;20] <=> COMPRESSED
     ⍝          [;19] <=> OFFLINE
     ⍝          [;18] <=> NOT CONTENT INDEXED
     ⍝          [;17] <=> ENCRYPTED
     ⍝          rest  <=> undocumented
     ⍝ 1⊃rslt <=> 7 column numeric matrix expressing the file creation time in ⎕TS format
     ⍝         if the file system does not support this then all columns are 0
     ⍝ 2⊃rslt <=> 7 column numeric matrix expressing the file last access time in ⎕TS format
     ⍝         if the file system does not support this then all columns are 0
     ⍝ 3⊃rslt <=> 7 column numeric matrix expressing the file last write time in ⎕TS format
     ⍝ 4⊃rslt <=> numeric vector giving the file size accurate up to 53 bits
     ⍝ 5⊃rslt <=> vector of character vectors giving the file names
     ⍝ 6⊃rslt <=> vector of character vectors giving the 8.3 file name for file systems
     ⍝         where it is appropriate and different from the file name
      ⎕IO←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          :If 0=⎕NC'amsk'
              amsk←32⍴1
          :EndIf
          _FindDefine
          handle rslt←_FindFirstFile path
          :If 0=handle
              ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
          :EndIf
          rslt←,⊂rslt
          :While 1=0⊃ok next←_FindNextFile handle
              rslt,←⊂next
          :EndWhile
          :If 0 18∨.≠ok next
              ('ntdir error:',⍕next)⎕SIGNAL 11   ⍝ DOMAIN
          :EndIf
          ok←FindClose handle
          rslt←↓[0]↑rslt
          (0⊃rslt)←⍉attrs←(32⍴2)⊤0⊃rslt                ⍝ Get attributes into bits
          rslt←(amsk∧.≥attrs)∘⌿¨rslt                ⍝ bin unwanted files and info
          rslt[1 2 3]←↑¨_Filetime_to_TS¨¨rslt[1 2 3]    ⍝ put times into ⎕ts format
          (4⊃rslt)←0(2*32)⊥⍉↑4⊃rslt                    ⍝ combine size elements
          rslt/⍨←5≠⍳8                               ⍝ bin the reserved elements
      :EndSelect
    ∇

    ∇ r←Exists name
     ⍝ Does file exist?
      r←1
      :Trap 22
          :Trap 19 ⍝ file access error means file exists
              ⎕NUNTIE(unixfix name)⎕NTIE 0
          :EndTrap
      :Else
          r←0
      :EndTrap
    ∇

    ∇ r←GetCurrentDirectory;GCD;GetLastError
     ⍝ Get Current Directory
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          r←⊃_SH'pwd'
      :Case 'Win'
          'GCD'⎕NA'I kernel32.C32∣GetCurrentDirectory* I4 >0T'
          :If 0≠1⊃r←GCD 256 256
              r←2⊃r
          :Else
              ⎕NA'I4 kernel32.C32|GetLastError'
              11 ⎕SIGNAL⍨'GetCurrentDirectory error:',⍕GetLastError
          :EndIf
      :EndSelect
    ∇

    ∇ r←GetText name;tn
     ⍝ Read a text file as single byte text
      tn←(unixfix name)⎕NTIE 0 ⋄ r←⎕NREAD tn(⎕DR' ')(⎕NSIZE tn) ⋄ ⎕NUNTIE tn
    ∇

    ∇ r←LikelyURL w
      →0↓⍨r←(⎕DR w)∊80 82
      r←{(0∊⍴⍵)<∧/⍵∊'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%-._~:/?#[]@!$&''()*+,;='}w ⍝ identify likely URIs
    ∇

    ∇ r←{file}List path;z;rslt;handle;next;ok;attrs;⎕IO;FindFirstFileX;FindNextFileX;FindClose;FileTimeToLocalFileTime;FileTimeToSystemTime;GetLastError;isFile;filter
    ⍝ Return matrix containing
    ⍝ [;0] Name [;1] Length [;2] LastAccessTime [;3] IsDirectory
      ⎕IO←0
      :If isFile←2=⎕NC'file' ⋄ filter←(0<⍴,file)/'/',file ⋄ :Else ⋄ filter←'/*' ⋄ :EndIf
      filter←('/\'∊⍨¯1↑path)↓filter
      r←0 4⍴'' 0 0 0

      :Select APLVersion
      :Case '*nix'
          →(0∊⍴rslt←1 _SH'ls -al --time-style=full-iso ',unixfix path,isFile/filter)⍴0
          rslt←↑rslt
          rslt←' ',('total '≡6⍴rslt)↓[0]rslt
          →(0=1↑⍴r←((1↑⍴rslt),4)⍴0)⍴0
          z←∧⌿' '=rslt ⍝ entirely blank columns
          z←z∧10>+\z    ⍝ Do not split file names
          rslt←z⊂rslt
          r[;3]←'d'=(0⊃rslt)[;1]                 ⍝ IsDirectory
          r[;1]←(~r[;3])×1⊃⎕VFI,4⊃rslt ⍝ Size
          z←,(5⊃rslt),6⊃rslt ⋄ ((z∊'-:')/z)←' ' ⋄ z←((1↑⍴r),6)⍴1⊃⎕VFI z
          r[;2]←↓⌊z,1000×1|z[;5]                ⍝ Add msec to Timestamp
          r[;0]←{(⌽~∨\⌽⍵='/')/⍵}¨{(-+/∧\' '=⌽⍵)↓¨↓⍵}0 1↓8⊃rslt    ⍝ Name

      :Case 'Mac'
          →(0∊⍴rslt←1 _SH'stat -lt "%F %T %z" ',unixfix path,isFile/filter)⍴0
          r←((1↑⍴rslt),4)⍴0
          rslt←↑{⎕ML←3 ⋄ ⍵⊂⍨~{⍵∧9>+\⍵}' '=⍵}¨rslt
          r[;3]←'d'=0⊃¨rslt[;1]                 ⍝ IsDirectory
          r[;1]←(~r[;3])×1⊃⎕VFI¨rslt[;4]        ⍝ Size
          z←↑∊¨↓{w←⍵ ⋄ ((w∊'-:')/w)←' ' ⋄ 1⊃⎕VFI w}¨rslt[;5 6] ⍝
          r[;2]←↓z,0                            ⍝ 0 msec for MacOS to Timestamp
          r[;0]←rslt[;8]                        ⍝ Name

      :Case 'Win'
      ⍝ See DirX for explanations of results of _FindNextFile etc
          _FindDefine
          handle rslt←_FindFirstFile path,filter
          :If 0=handle
              :Return ⍝ ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
          :EndIf
          rslt←,⊂rslt
          :While 1=0⊃ok next←_FindNextFile handle
              rslt,←⊂next
          :EndWhile
          :If 0 18∨.≠ok next
              ('ntdir error:',⍕next)⎕SIGNAL 11   ⍝ DOMAIN
          :EndIf
          ok←FindClose handle
          rslt←↓[0]↑rslt
          →(0=1↑⍴r←((1↑⍴0⊃rslt),4)⍴0)⍴0
          (0⊃rslt)←⍉attrs←(32⍴2)⊤0⊃rslt                ⍝ Get attributes into bits
          r[;3]←(0⊃rslt)[;27]              ⍝ IsDirectory?
          r[;1]←0(2*32)⊥⍉↑4⊃rslt          ⍝ combine size elements
          r[;2]←_Filetime_to_TS¨3⊃rslt     ⍝ As ⎕TS vector
          r[;0]←6⊃rslt                     ⍝ Name
      :EndSelect
      r←r[⍋↑r[;0];]
    ∇

    ∇ MkDir path;CreateDirectory;GetLastError;err
      ⍝ Create a folder
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          :If ~DirExists path
              1 _SH'mkdir ',unixfix path
              ('mkdir error on ',path)⎕SIGNAL 11/⍨~DirExists path
          :EndIf
      :Case 'Win'
          ⎕NA'I kernel32.C32∣CreateDirectory* <0T I4' ⍝ Try for best function
          →(0≠CreateDirectory path 0)⍴0 ⍝ 0 means "default security attributes"
          ⎕NA'I4 kernel32.C32|GetLastError'
          err ⎕SIGNAL⍨'CreateDirectory error:',⍕err←GetLastError
      :EndSelect
    ∇

    ∇ Move filenames;MoveFileX;MoveFileExA;GetLastError;err
    ⍝ Move (from to) - move/rename file
      :Select APLVersion
      :Case '*nix' 'Mac'
          ∘
      :Case 'Win'
          ⎕NA'I kernel32.C32∣MoveFileEx* <0T <0T I4' ⍝ Try for best function
          :If 0≠MoveFileExA filenames,3 ⍝ REPLACE_EXISTING (1) + COPY_ALLOWED (2)
              :Return
          :EndIf
          ⎕NA'I4 kernel32.C32|GetLastError'
          :Select err←GetLastError
          :Case 120                     ⍝ ERROR_CALL_NOT_IMPLIMENTED
              'MoveFileX'⎕NA'I Kernel32.C32∣MoveFile* <0T <0T' ⍝ accept 2nd best - win 95
              :If 0≠MoveFileX filenames
                  :Return
              :EndIf
              err←GetLastError
          :EndSelect
          11 ⎕SIGNAL⍨'MoveFile error:',⍕err
      :EndSelect
    ∇

    ∇ r←text PutText name;tn
     ⍝ Write text to file (must be single byte text)
      :Trap 0
          tn←name ⎕NCREATE 0
      :Else
          tn←name ⎕NTIE 0
          0 ⎕NRESIZE tn
      :EndTrap

      r←text ⎕NAPPEND tn(⎕DR' ')
      ⎕NUNTIE tn
    ∇

    ∇ RmDir path;RemoveDirectoryA;GetLastError
     ⍝ Remove folder/directory
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          ⎕NA'I kernel32.C32∣RemoveDirectory* <0T'
          →(0≠RemoveDirectory,⊂path)⍴0
          ⎕NA'I4 kernel32.C32|GetLastError'
          11 ⎕SIGNAL⍨'RemoveDirectory error:',⍕GetLastError
      :EndSelect
    ∇

    ∇ SetCurrentDirectory path;SCD;GetLastError
     ⍝ Set Current Directory
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          'SCD'⎕NA'I kernel32.C32∣SetCurrentDirectory* <0T'
          →(0≠SCD,⊂path)⍴0
          ⎕NA'I4 kernel32.C32|GetLastError'
          11 ⎕SIGNAL⍨'SetCurrentDirectory error:',⍕GetLastError
      :EndSelect
    ∇

    ∇ r←{noext}SplitFilename filename;filesep;mask;path;file;ext
    ⍝ splits a filename into: path name ext
      noext←{6::0 ⋄ noext}''
      filesep←(~IsWin)↓'\/'
      mask←⌽∨\⌽filename∊filesep
      path←mask/filename
      file←(~mask)/filename
      :If noext
          r←path file
      :Else
          mask←∨\⌽<\⌽'.'=file
          ext←mask/file
          file←(~mask)/file
          r←path file ext
      :EndIf
    ∇

    :Section Windows
    ∇ rslt←_Filetime_to_TS filetime;⎕IO
      :If 1≠0⊃rslt←FileTimeToLocalFileTime filetime(⎕IO←0)
      :OrIf 1≠0⊃rslt←FileTimeToSystemTime(1⊃rslt)0
          rslt←0 0                   ⍝ if either call failed then zero the time elements
      :EndIf
      rslt←1 1 0 1 1 1 1 1/1⊃rslt    ⍝ remove day of week
    ∇

    ∇ _FindDefine;WIN32_FIND_DATA
      WIN32_FIND_DATA←'{I4 {I4 I4} {I4 I4} {I4 I4} {U4 U4} {I4 I4} T[260] T[14]}'
      'FindFirstFileX'⎕NA'I4 kernel32.C32|FindFirstFile* <0T >',WIN32_FIND_DATA
      'FindNextFileX'⎕NA'U4 kernel32.C32|FindNextFile* I4 >',WIN32_FIND_DATA
      ⎕NA'kernel32.C32|FindClose I4'
      ⎕NA'I4 kernel32.C32|FileTimeToLocalFileTime <{I4 I4} >{I4 I4}'
      ⎕NA'I4 kernel32.C32|FileTimeToSystemTime <{I4 I4} >{I2 I2 I2 I2 I2 I2 I2 I2}'
      ⎕NA'I4 kernel32.C32∣GetLastError'
    ∇

    ∇ rslt←_FindFirstFile name;⎕IO
      rslt←FindFirstFileX name(⎕IO←0)
      :If ¯1=0⊃rslt                   ⍝ INVALID_HANDLE_VALUE
          rslt←0 GetLastError
      :Else
          (1 6⊃rslt)_FindTrim←0        ⍝ shorten the file name at the null delimiter
          (1 7⊃rslt)_FindTrim←0        ⍝ and for the alternate name
      :EndIf
    ∇

    ∇ rslt←_FindNextFile handle;⎕IO
      rslt←FindNextFileX handle(⎕IO←0)
      :If 1≠0⊃rslt
          rslt←0 GetLastError
      :Else
          (1 6⊃rslt)_FindTrim←0             ⍝ shorten the filename
          (1 7⊃rslt)_FindTrim←0             ⍝ shorten the alternate name
      :EndIf
    ∇

    ∇ name←name _FindTrim ignored;⎕IO
     ⍝ Truncates a character vector at the null delimiting byte.
     ⍝ The null is not included in the result.
      ⎕IO←0
      name↑⍨←name⍳⎕UCS 0
    ∇

    ∇ {r}←{suppress}_CMD cmd
      :If 0=⎕NC'suppress' ⋄ suppress←0 ⋄ :EndIf
      r←''
      :Trap 0
          r←⎕CMD cmd
      :Else
          ('shell command failed: ',cmd)⎕SIGNAL 11/⍨~suppress
      :EndTrap
    ∇

    ∇ r←IsWin
      r←'Win'≡APLVersion
    ∇

    :endsection

    :section *nix
    ∇ {r}←{suppress}_SH cmd
    ⍝ SH cover to suppress any error messages
    ⍝ suppress will suppress error from being signaled
      :If 0=⎕NC'suppress' ⋄ suppress←0 ⋄ :EndIf
      r←''
      :Trap 0
          r←⎕SH cmd,' 2>/dev/null'
      :Else
          ('shell command failed: ',cmd)⎕SIGNAL 11/⍨~suppress
      :EndTrap
    ∇

    ∇ f←unixfix f
    ⍝ replaces Windows file separator \ with Unix file separator /
    ⍝ this approach is mindnumbingly simple and probably dangerous
    ⍝ which is why we call unixfix very cautiously
      :If (⊂APLVersion)∊'*nix' 'Mac' ⋄ ((f='\')/f)←'/' ⋄ :EndIf
    ∇

    ∇ r←APLVersion
      :Select 3↑⊃'.'⎕WG'APLVersion'
      :CaseList 'Lin' 'AIX' 'Sol'
          r←'*nix'
      :Case 'Win'
          r←'Win'
      :Case 'Mac'
          r←'Mac'
      :Else
          ... ⍝ unknown version
      :EndSelect
    ∇
    :endsection

    ∇ r←a FREAD w;t
      t←w ⎕FSTIE 0
      r←⎕FREAD t a
      ⎕FUNTIE t
    ∇
:EndNamespace

:Namespace Files

⍝ Provides cover functions for many file operations
⍝ Eventually all will be implemented for both Windows and *nix

    (⎕IO ⎕ML)←1

    ∇ r←text AppendText name;tn
     ⍝ Append text to existing file (must be single byte text)
      tn←name ⎕NTIE 0
      r←text ⎕NAPPEND tn(⎕DR' ')
      ⎕NUNTIE tn
    ∇

    ∇ (tn name)←{ext}CreateTemp folder;cnt
     ⍝ Create a temp file
     ⍝ folder - folder to create temp file in
     ⍝ ext - file extension
      :If 0=⎕NC'ext' ⋄ ext←'tmp' ⋄ :EndIf
      :If 0∊⍴ext ⋄ ext←'tmp' ⋄ :EndIf
      cnt←tn←0
      :Repeat
          name←folder,('/\'∊⍨¯1↑folder)↓'/',⎕A[?8⍴⍴⎕A],'.',ext
          :Trap 22 ⋄ tn←name ⎕NCREATE 0 ⋄ :EndTrap
          cnt+←1
      :Until (cnt>20)∨tn≠0
      'Unable to create temporary file'⎕SIGNAL tn↓22
    ∇

    ∇ {protect}Copy FmTo;CopyFileX;GetLastError ⍝ Copy file Fm -> To
      :Select APLVersion
      :Case '*nix'
          ∘
      :Case 'Win'
          :If 0=⎕NC'protect'     ⍝ Copy fails if <protect> and 'To' exists.
              protect←0          ⍝ Default unprotected copy.
          :EndIf
          'CopyFileX'⎕NA'I kernel32.C32∣CopyFile* <0T <0T I'
          :If 0=CopyFileX FmTo,protect
              ⎕NA'I4 kernel32.C32|GetLastError'
              11 ⎕SIGNAL⍨'CopyFile error:',⍕GetLastError
          :EndIf
      :EndSelect
    ∇

    ∇ Delete name;DeleteFileX;GetLastError;FindFirstFile;FindNextFile;FindClose;handle;rslt;ok;next;⎕IO;path
      ⎕IO←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          _SH'rm ',unixfix name
      :Case 'Win'
          'DeleteFileX'⎕NA'I kernel32.C32∣DeleteFile* <0T'
          ⎕NA'I4 kernel32.C32|GetLastError'
          :If ∨/'*?'∊name ⍝ wildcards?
              path←{(⌽∨\⌽⍵∊'\/')/⍵}name
              _FindDefine
              handle rslt←_FindFirstFile name
              :If 0=handle
                  :Return ⍝ ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
              :EndIf
              :If '.'≠⊃6⊃rslt
                  {}DeleteFileX⊂path,6⊃rslt
              :EndIf
              :While 1=0⊃ok next←_FindNextFile handle
                  :If '.'≠⊃6⊃next
                      {}DeleteFileX⊂path,6⊃next
                  :EndIf
              :EndWhile
              :If 0 18∨.≠ok next
                  ('ntdir error:',⍕next)⎕SIGNAL 11   ⍝ DOMAIN
              :EndIf
              {}FindClose handle
          :Else
              :If 0=DeleteFileX⊂name
                  11 ⎕SIGNAL⍨'DeleteFile error:',⍕GetLastError
              :EndIf
          :EndIf
      :EndSelect
    ∇

    ∇ rslt←{amsk}Dir path;handle;next;ok;⎕IO;attrs;FindFirstFileX;FindNextFileX;FindClose;FileTimeToLocalFileTime;FileTimeToSystemTime;GetLastError
     ⍝ 'FIX ME'⎕SIGNAL 11
     ⍝ Amsk is a 32 element bool attribute mask.
     ⍝ Only files with attributes corresponding to 1-s in the mask will be returned.
     ⍝ '*'s mark default attribute mask.
     ⍝
     ⍝        * [31] <=> READONLY
     ⍝          [30] <=> HIDDEN
     ⍝        * [29] <=> SYSTEM
     ⍝          [28] <=> undocumented
     ⍝        * [27] <=> DIRECTORY
     ⍝        * [26] <=> ARCHIVE
     ⍝          [25] <=> DEVICE
     ⍝        * [24] <=> NORMAL - only set if no other bits are set
     ⍝        * [23] <=> TEMPORARY
     ⍝        * [22] <=> SPARSE FILE
     ⍝        * [21] <=> REPARSE POINT
     ⍝        * [20] <=> COMPRESSED
     ⍝        * [19] <=> OFFLINE
     ⍝        * [18] <=> NOT CONTENT INDEXED
     ⍝        * [17] <=> ENCRYPTED
     ⍝        * rest <=> undocumented (but in the default set so that
     ⍝                   Microsoft can extend them)
    ⍝ rslt is a vector of character vectors of filenames

      ⎕IO←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          :If 0=⎕NC'amsk'
              amsk←~(⍳32)∊30 28 25   ⍝ Default attribute mask.
          :EndIf
          _FindDefine
          handle rslt←_FindFirstFile path
          :If 0=handle
              ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
          :EndIf
          rslt←,⊂rslt
          :While 1=0⊃ok next←_FindNextFile handle
              rslt,←⊂next
          :EndWhile
          :If 0 18∨.≠ok next
              ('ntdir error:',⍕next)⎕SIGNAL 11       ⍝ DOMAIN
          :EndIf
          ok←FindClose handle
          rslt←↓[0]↑rslt
          attrs←(32⍴2)⊤0⊃rslt                        ⍝ Get attributes into bits
          rslt←(amsk∧.≥attrs)⌿6⊃rslt                 ⍝ bin unwanted files and info
      :EndSelect
    ∇

    ∇ r←DirExists path
      r←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          :Trap 11
              r←0<⍴_SH'ls -adl ',unixfix path
          :EndTrap
      :Case 'Win'
          r←0<⍬⍴⍴'.'List path
      :EndSelect
    ∇

    ∇ rslt←{amsk}DirX path;handle;next;ok;attrs;⎕IO;FindFirstFileX;FindNextFileX;FindClose;FileTimeToLocalFileTime;FileTimeToSystemTime;GetLastError
     ⍝ 'FIX ME'⎕SIGNAL 11
     ⍝ Amsk is a 32 element bool attribute mask.
     ⍝ Only files with attributes corresponding to 1-s in the mask will be returned.
     ⍝ Amsk defaults to all attributes.
     ⍝ 0⊃rslt <=> 32 column boolean matrix of attribute bits
     ⍝          [;31] <=> READONLY
     ⍝          [;30] <=> HIDDEN
     ⍝          [;29] <=> SYSTEM
     ⍝          [;28] <=> undocumented
     ⍝          [;27] <=> DIRECTORY
     ⍝          [;26] <=> ARCHIVE
     ⍝          [;25] <=> undocumented
     ⍝          [;24] <=> NORMAL - only set if no other bits are set
     ⍝          [;23] <=> TEMPORARY
     ⍝          [;22] <=> SPARSE FILE
     ⍝          [;21] <=> REPARSE POINT
     ⍝          [;20] <=> COMPRESSED
     ⍝          [;19] <=> OFFLINE
     ⍝          [;18] <=> NOT CONTENT INDEXED
     ⍝          [;17] <=> ENCRYPTED
     ⍝          rest  <=> undocumented
     ⍝ 1⊃rslt <=> 7 column numeric matrix expressing the file creation time in ⎕TS format
     ⍝         if the file system does not support this then all columns are 0
     ⍝ 2⊃rslt <=> 7 column numeric matrix expressing the file last access time in ⎕TS format
     ⍝         if the file system does not support this then all columns are 0
     ⍝ 3⊃rslt <=> 7 column numeric matrix expressing the file last write time in ⎕TS format
     ⍝ 4⊃rslt <=> numeric vector giving the file size accurate up to 53 bits
     ⍝ 5⊃rslt <=> vector of character vectors giving the file names
     ⍝ 6⊃rslt <=> vector of character vectors giving the 8.3 file name for file systems
     ⍝         where it is appropriate and different from the file name
      ⎕IO←0
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          :If 0=⎕NC'amsk'
              amsk←32⍴1
          :EndIf
          _FindDefine
          handle rslt←_FindFirstFile path
          :If 0=handle
              ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
          :EndIf
          rslt←,⊂rslt
          :While 1=0⊃ok next←_FindNextFile handle
              rslt,←⊂next
          :EndWhile
          :If 0 18∨.≠ok next
              ('ntdir error:',⍕next)⎕SIGNAL 11   ⍝ DOMAIN
          :EndIf
          ok←FindClose handle
          rslt←↓[0]↑rslt
          (0⊃rslt)←⍉attrs←(32⍴2)⊤0⊃rslt                ⍝ Get attributes into bits
          rslt←(amsk∧.≥attrs)∘⌿¨rslt                ⍝ bin unwanted files and info
          rslt[1 2 3]←↑¨_Filetime_to_TS¨¨rslt[1 2 3]    ⍝ put times into ⎕ts format
          (4⊃rslt)←0(2*32)⊥⍉↑4⊃rslt                    ⍝ combine size elements
          rslt/⍨←5≠⍳8                               ⍝ bin the reserved elements
      :EndSelect
    ∇

    ∇ r←Exists name
     ⍝ Does file exist?
      r←1
      :Trap 22
          :Trap 19 ⍝ file access error means file exists
              ⎕NUNTIE(unixfix name)⎕NTIE 0
          :EndTrap
      :Else
          r←0
      :EndTrap
    ∇

    ∇ r←GetCurrentDirectory;GCD;GetLastError
     ⍝ Get Current Directory
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          r←⊃_SH'pwd'
      :Case 'Win'
          'GCD'⎕NA'I kernel32.C32∣GetCurrentDirectory* I4 >0T'
          :If 0≠1⊃r←GCD 256 256
              r←2⊃r
          :Else
              ⎕NA'I4 kernel32.C32|GetLastError'
              11 ⎕SIGNAL⍨'GetCurrentDirectory error:',⍕GetLastError
          :EndIf
      :EndSelect
    ∇

    ∇ r←GetText name;tn
     ⍝ Read a text file as single byte text
      tn←(unixfix name)⎕NTIE 0 ⋄ r←⎕NREAD tn(⎕DR' ')(⎕NSIZE tn) ⋄ ⎕NUNTIE tn
    ∇

    ∇ r←LikelyURL w
      →0↓⍨r←(⎕DR w)∊80 82
      r←{(0∊⍴⍵)<∧/⍵∊'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%-._~:/?#[]@!$&''()*+,;='}w ⍝ identify likely URIs
    ∇

    ∇ r←{file}List path;z;rslt;handle;next;ok;attrs;⎕IO;FindFirstFileX;FindNextFileX;FindClose;FileTimeToLocalFileTime;FileTimeToSystemTime;GetLastError;isFile;filter
    ⍝ Return matrix containing
    ⍝ [;0] Name [;1] Length [;2] LastAccessTime [;3] IsDirectory
      ⎕IO←0
      :If isFile←2=⎕NC'file' ⋄ filter←(0<⍴,file)/'/',file ⋄ :Else ⋄ filter←'/*' ⋄ :EndIf
      filter←('/\'∊⍨¯1↑path)↓filter
      r←0 4⍴'' 0 0 0

      :Select APLVersion
      :Case '*nix'
          →(0∊⍴rslt←1 _SH'ls -al --time-style=full-iso ',unixfix path,isFile/filter)⍴0
          rslt←↑rslt
          rslt←' ',('total '≡6⍴rslt)↓[0]rslt
          →(0=1↑⍴r←((1↑⍴rslt),4)⍴0)⍴0
          z←∧⌿' '=rslt ⍝ entirely blank columns
          z←z∧10>+\z    ⍝ Do not split file names
          rslt←z⊂rslt
          r[;3]←'d'=(0⊃rslt)[;1]                 ⍝ IsDirectory
          r[;1]←(~r[;3])×1⊃⎕VFI,4⊃rslt ⍝ Size
          z←,(5⊃rslt),6⊃rslt ⋄ ((z∊'-:')/z)←' ' ⋄ z←((1↑⍴r),6)⍴1⊃⎕VFI z
          r[;2]←↓⌊z,1000×1|z[;5]                ⍝ Add msec to Timestamp
          r[;0]←{(⌽~∨\⌽⍵='/')/⍵}¨{(-+/∧\' '=⌽⍵)↓¨↓⍵}0 1↓8⊃rslt    ⍝ Name

      :Case 'Mac'
          →(0∊⍴rslt←1 _SH'stat -lt "%F %T %z" ',unixfix path,isFile/filter)⍴0
          r←((1↑⍴rslt),4)⍴0
          rslt←↑{⎕ML←3 ⋄ ⍵⊂⍨~{⍵∧9>+\⍵}' '=⍵}¨rslt
          r[;3]←'d'=0⊃¨rslt[;1]                 ⍝ IsDirectory
          r[;1]←(~r[;3])×1⊃⎕VFI¨rslt[;4]        ⍝ Size
          z←↑∊¨↓{w←⍵ ⋄ ((w∊'-:')/w)←' ' ⋄ 1⊃⎕VFI w}¨rslt[;5 6] ⍝
          r[;2]←↓z,0                            ⍝ 0 msec for MacOS to Timestamp
          r[;0]←rslt[;8]                        ⍝ Name

      :Case 'Win'
      ⍝ See DirX for explanations of results of _FindNextFile etc
          _FindDefine
          handle rslt←_FindFirstFile path,filter
          :If 0=handle
              :Return ⍝ ('ntdir error:',⍕rslt)⎕SIGNAL 102      ⍝ file not found
          :EndIf
          rslt←,⊂rslt
          :While 1=0⊃ok next←_FindNextFile handle
              rslt,←⊂next
          :EndWhile
          :If 0 18∨.≠ok next
              ('ntdir error:',⍕next)⎕SIGNAL 11   ⍝ DOMAIN
          :EndIf
          ok←FindClose handle
          rslt←↓[0]↑rslt
          →(0=1↑⍴r←((1↑⍴0⊃rslt),4)⍴0)⍴0
          (0⊃rslt)←⍉attrs←(32⍴2)⊤0⊃rslt                ⍝ Get attributes into bits
          r[;3]←(0⊃rslt)[;27]              ⍝ IsDirectory?
          r[;1]←0(2*32)⊥⍉↑4⊃rslt          ⍝ combine size elements
          r[;2]←_Filetime_to_TS¨3⊃rslt     ⍝ As ⎕TS vector
          r[;0]←6⊃rslt                     ⍝ Name
      :EndSelect
      r←r[⍋↑r[;0];]
    ∇

    ∇ MkDir path;CreateDirectory;GetLastError;err
      ⍝ Create a folder
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          :If ~DirExists path
              1 _SH'mkdir ',unixfix path
              ('mkdir error on ',path)⎕SIGNAL 11/⍨~DirExists path
          :EndIf
      :Case 'Win'
          ⎕NA'I kernel32.C32∣CreateDirectory* <0T I4' ⍝ Try for best function
          →(0≠CreateDirectory path 0)⍴0 ⍝ 0 means "default security attributes"
          ⎕NA'I4 kernel32.C32|GetLastError'
          err ⎕SIGNAL⍨'CreateDirectory error:',⍕err←GetLastError
      :EndSelect
    ∇

    ∇ Move filenames;MoveFileX;MoveFileExA;GetLastError;err
    ⍝ Move (from to) - move/rename file
      :Select APLVersion
      :Case '*nix' 'Mac'
          ∘
      :Case 'Win'
          ⎕NA'I kernel32.C32∣MoveFileEx* <0T <0T I4' ⍝ Try for best function
          :If 0≠MoveFileExA filenames,3 ⍝ REPLACE_EXISTING (1) + COPY_ALLOWED (2)
              :Return
          :EndIf
          ⎕NA'I4 kernel32.C32|GetLastError'
          :Select err←GetLastError
          :Case 120                     ⍝ ERROR_CALL_NOT_IMPLIMENTED
              'MoveFileX'⎕NA'I Kernel32.C32∣MoveFile* <0T <0T' ⍝ accept 2nd best - win 95
              :If 0≠MoveFileX filenames
                  :Return
              :EndIf
              err←GetLastError
          :EndSelect
          11 ⎕SIGNAL⍨'MoveFile error:',⍕err
      :EndSelect
    ∇

    ∇ r←text PutText name;tn
     ⍝ Write text to file (must be single byte text)
      :Trap 0
          tn←name ⎕NCREATE 0
      :Else
          tn←name ⎕NTIE 0
          0 ⎕NRESIZE tn
      :EndTrap

      r←text ⎕NAPPEND tn(⎕DR' ')
      ⎕NUNTIE tn
    ∇

    ∇ RmDir path;RemoveDirectoryA;GetLastError
     ⍝ Remove folder/directory
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          ⎕NA'I kernel32.C32∣RemoveDirectory* <0T'
          →(0≠RemoveDirectory,⊂path)⍴0
          ⎕NA'I4 kernel32.C32|GetLastError'
          11 ⎕SIGNAL⍨'RemoveDirectory error:',⍕GetLastError
      :EndSelect
    ∇

    ∇ SetCurrentDirectory path;SCD;GetLastError
     ⍝ Set Current Directory
      :Select APLVersion
      :CaseList '*nix' 'Mac'
          ∘
      :Case 'Win'
          'SCD'⎕NA'I kernel32.C32∣SetCurrentDirectory* <0T'
          →(0≠SCD,⊂path)⍴0
          ⎕NA'I4 kernel32.C32|GetLastError'
          11 ⎕SIGNAL⍨'SetCurrentDirectory error:',⍕GetLastError
      :EndSelect
    ∇

    ∇ r←{noext}SplitFilename filename;filesep;mask;path;file;ext
    ⍝ splits a filename into: path name ext
      noext←{6::0 ⋄ noext}''
      filesep←(~IsWin)↓'\/'
      mask←⌽∨\⌽filename∊filesep
      path←mask/filename
      file←(~mask)/filename
      :If noext
          r←path file
      :Else
          mask←∨\⌽<\⌽'.'=file
          ext←mask/file
          file←(~mask)/file
          r←path file ext
      :EndIf
    ∇

    :Section Windows
    ∇ rslt←_Filetime_to_TS filetime;⎕IO
      :If 1≠0⊃rslt←FileTimeToLocalFileTime filetime(⎕IO←0)
      :OrIf 1≠0⊃rslt←FileTimeToSystemTime(1⊃rslt)0
          rslt←0 0                   ⍝ if either call failed then zero the time elements
      :EndIf
      rslt←1 1 0 1 1 1 1 1/1⊃rslt    ⍝ remove day of week
    ∇

    ∇ _FindDefine;WIN32_FIND_DATA
      WIN32_FIND_DATA←'{I4 {I4 I4} {I4 I4} {I4 I4} {U4 U4} {I4 I4} T[260] T[14]}'
      'FindFirstFileX'⎕NA'I4 kernel32.C32|FindFirstFile* <0T >',WIN32_FIND_DATA
      'FindNextFileX'⎕NA'U4 kernel32.C32|FindNextFile* I4 >',WIN32_FIND_DATA
      ⎕NA'kernel32.C32|FindClose I4'
      ⎕NA'I4 kernel32.C32|FileTimeToLocalFileTime <{I4 I4} >{I4 I4}'
      ⎕NA'I4 kernel32.C32|FileTimeToSystemTime <{I4 I4} >{I2 I2 I2 I2 I2 I2 I2 I2}'
      ⎕NA'I4 kernel32.C32∣GetLastError'
    ∇

    ∇ rslt←_FindFirstFile name;⎕IO
      rslt←FindFirstFileX name(⎕IO←0)
      :If ¯1=0⊃rslt                   ⍝ INVALID_HANDLE_VALUE
          rslt←0 GetLastError
      :Else
          (1 6⊃rslt)_FindTrim←0        ⍝ shorten the file name at the null delimiter
          (1 7⊃rslt)_FindTrim←0        ⍝ and for the alternate name
      :EndIf
    ∇

    ∇ rslt←_FindNextFile handle;⎕IO
      rslt←FindNextFileX handle(⎕IO←0)
      :If 1≠0⊃rslt
          rslt←0 GetLastError
      :Else
          (1 6⊃rslt)_FindTrim←0             ⍝ shorten the filename
          (1 7⊃rslt)_FindTrim←0             ⍝ shorten the alternate name
      :EndIf
    ∇

    ∇ name←name _FindTrim ignored;⎕IO
     ⍝ Truncates a character vector at the null delimiting byte.
     ⍝ The null is not included in the result.
      ⎕IO←0
      name↑⍨←name⍳⎕UCS 0
    ∇

    ∇ {r}←{suppress}_CMD cmd
      :If 0=⎕NC'suppress' ⋄ suppress←0 ⋄ :EndIf
      r←''
      :Trap 0
          r←⎕CMD cmd
      :Else
          ('shell command failed: ',cmd)⎕SIGNAL 11/⍨~suppress
      :EndTrap
    ∇

    ∇ r←IsWin
      r←'Win'≡APLVersion
    ∇

    :endsection

    :section *nix
    ∇ {r}←{suppress}_SH cmd
    ⍝ SH cover to suppress any error messages
    ⍝ suppress will suppress error from being signaled
      :If 0=⎕NC'suppress' ⋄ suppress←0 ⋄ :EndIf
      r←''
      :Trap 0
          r←⎕SH cmd,' 2>/dev/null'
      :Else
          ('shell command failed: ',cmd)⎕SIGNAL 11/⍨~suppress
      :EndTrap
    ∇

    ∇ f←unixfix f
    ⍝ replaces Windows file separator \ with Unix file separator /
    ⍝ this approach is mindnumbingly simple and probably dangerous
    ⍝ which is why we call unixfix very cautiously
      :If '*nix'≡APLVersion ⋄ ((f='\')/f)←'/' ⋄ :EndIf
    ∇

    ∇ r←APLVersion
      :Select 3↑⊃'.'⎕WG'APLVersion'
      :CaseList 'Lin' 'AIX' 'Sol'
          r←'*nix'
      :Case 'Win'
          r←'Win'
      :Case 'Mac'
          r←'Mac'
      :Else
          ... ⍝ unknown version
      :EndSelect
    ∇
    :endsection

    ∇ r←a FREAD w;t
      t←w ⎕FSTIE 0
      r←⎕FREAD t a
      ⎕FUNTIE t
    ∇
:EndNamespace