<commit_before>###############################################################################
#
# SageMathCloud: A collaborative web-based interface to Sage, IPython, LaTeX and the Terminal.
#
#    Copyright (C) 2016, Sagemath Inc.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

async = require('async')

{React, ReactDOM, rclass, rtypes, is_redux, is_redux_actions, redux} = require('./smc-react')
{Alert, Button, ButtonToolbar, Checkbox, Col, FormControl, FormGroup, ControlLabel, InputGroup, OverlayTrigger, Popover, Tooltip, Row, Well} = require('react-bootstrap')
{HelpEmailLink, SiteName, CompanyName, PricingUrl, PolicyTOSPageUrl, PolicyIndexPageUrl, PolicyPricingPageUrl} = require('./customize')

# injected by webpack, but not for react-static renderings (ATTN don't assign to uppercase vars!)
smc_version = SMC_VERSION ? 'N/A'
build_date  = BUILD_DATE  ? 'N/A'
smc_git_rev = SMC_GIT_REV ? 'N/A'

Combobox    = require('react-widgets/lib/Combobox')

misc        = require('smc-util/misc')
immutable   = require('immutable')
underscore  = require('underscore')

markdown    = require('./markdown')

{defaults, required} = misc

# base unit in pixel for margin/size/padding
exports.UNIT = UNIT = 15

# bootstrap blue background
exports.BS_BLUE_BGRND = "rgb(66, 139, 202)"

exports.SAGE_LOGO_COLOR = exports.BS_BLUE_BGRND

# Checks whether two immutable variables (either ImmutableJS objects or actual
# immutable types) are equal. Gives a warning and returns false (no matter what) if either variable is mutable.
immutable_equals_single = (a, b) ->
    if typeof(a) == "object" or typeof(b) == "object"
        if (is_redux(a) and is_redux(b)) or (is_redux_actions(a) and is_redux_actions(b))
            return a == b
        if immutable.Iterable.isIterable(a) and immutable.Iterable.isIterable(b)
            return immutable.is(a, b)
        if (a? and not b?) or (not a? and b?)
            # if one is undefined and the other is defined, they aren't equal
            return false
        console.warn("Using mutable object in ImmutablePureRenderMixin:", a, b)
        return false
    return a == b

immutable_equals = (objA, objB) ->
    if immutable.is(objA, objB)
        return true
    keysA = misc.keys(objA)
    keysB = misc.keys(objB)
    if keysA.length != keysB.length
        return false

    for key in keysA
        if not objB.hasOwnProperty(key) or not immutable_equals_single(objA[key], objB[key])
            return false
    return true

# Like PureRenderMixin, except only for immutable variables. Will always
# re-render if any props are mutable objects.
exports.ImmutablePureRenderMixin = ImmutablePureRenderMixin =
    shouldComponentUpdate: (nextProps, nextState) ->
        not immutable_equals(@props, nextProps) or not immutable_equals(@state, nextState)

# Gives components a setInterval method that takes a function and time x milliseconds
# then calls that function every x milliseconds. Automatically stops calling
# when component is unmounted. Can be called multiple times for multiple intervals.
exports.SetIntervalMixin =
    componentWillMount: ->
        @intervals = []
    setInterval: (fn, ms) ->
        @intervals.push setInterval fn, ms
    componentWillUnmount: ->
        @intervals.forEach clearInterval

exports.Space = Space = ->
    <span>&nbsp</span>

# Font Awesome component -- obviously TODO move to own file
# Converted from https://github.com/andreypopp/react-fa
exports.Icon = Icon = rclass
    displayName : 'Icon'

    propTypes :
        name       : rtypes.string
        size       : rtypes.oneOf(['lg', '2x', '3x', '4x', '5x'])
        rotate     : rtypes.oneOf(['45', '90', '135', '180', '225', '270', '315'])
        flip       : rtypes.oneOf(['horizontal', 'vertical'])
        spin       : rtypes.bool
        pulse      : rtypes.bool
        fixedWidth : rtypes.bool
        stack      : rtypes.oneOf(['1x', '2x'])
        inverse    : rtypes.bool
        className  : rtypes.string
        style      : rtypes.object
        onClick    : rtypes.func
        onMouseOver: rtypes.func
        onMouseOut : rtypes.func

    getDefaultProps: ->
        name    : 'square-o'
        onClick : ->

    render: ->
        {name, size, rotate, flip, spin, pulse, fixedWidth, stack, inverse, className, style} = @props
        # temporary until file_associations can be changed
        if name.slice(0, 3) == 'fa-'
            classNames = "fa #{name}"
        else
            classNames = "fa fa-#{name}"
        if size
            classNames += " fa-#{size}"
        if rotate
            classNames += " fa-rotate-#{rotate}"
        if flip
            classNames += " fa-flip-#{flip}"
        if fixedWidth
            classNames += ' fa-fw'
        if spin
            classNames += ' fa-spin'
        if pulse
            classNames += ' fa-pulse'
        if stack
            classNames += " fa-stack-#{stack}"
        if inverse
            classNames += ' fa-inverse'
        if className
            classNames += " #{className}"
        return <i style={style} className={classNames} onMouseOver={@props.onMouseOver} onMouseOut={@props.onMouseOut} onClick={@props.onClick}>{@props.children}</i>

# this Octicon icon class requires the CSS file in octicons/octicons/octicons.css (see landing.coffee)
exports.Octicon = rclass
    displayName : 'Octicon'

    propTypes :
        name   : rtypes.string.isRequired
        mega   : rtypes.bool
        spin   : rtypes.bool

    getDefaultProps: ->
        name : 'flame'
        mega : false
        spin : false

    render: ->
        classNames = ['octicon', "octicon-#{@props.name}"]
        if @props.spin
            classNames.push('spin-octicon')
        if @props.mega
            classNames.push('mega-octicon')
        return <span className={classNames.join(' ')} />

exports.Loading = Loading = rclass
    displayName : 'Misc-Loading'

    propTypes :
        style : rtypes.object

    render: ->
        <span style={@props.style}>
            <Icon name='circle-o-notch' spin /> Loading...
        </span>

exports.Saving = Saving = rclass
    displayName : 'Misc-Saving'

    render: ->
        <span><Icon name='circle-o-notch' spin /> Saving...</span>

closex_style =
    float      : 'right'
    marginLeft : '5px'

exports.CloseX = CloseX = rclass
    displayName : 'Misc-CloseX'

    propTypes :
        on_close : rtypes.func.isRequired
        style    : rtypes.object   # optional style for the icon itself

    render:->
        <a href='' style={closex_style} onClick={(e)=>e.preventDefault();@props.on_close()}>
            <Icon style={@props.style} name='times' />
        </a>


error_text_style =
    marginRight : '1ex'
    whiteSpace  : 'pre-line'
    maxWidth    : '80ex'

exports.ErrorDisplay = ErrorDisplay = rclass
    displayName : 'Misc-ErrorDisplay'

    propTypes :
        error   : rtypes.oneOfType([rtypes.string,rtypes.object])
        error_component : rtypes.any
        title   : rtypes.string
        style   : rtypes.object
        bsStyle : rtypes.string
        onClose : rtypes.func       # TODO: change to on_close everywhere...?

    render_close_button: ->
        <CloseX on_close={@props.onClose} style={fontSize:'11pt'} />

    render_title: ->
        <h4>{@props.title}</h4>

    render: ->
        if @props.style?
            style = misc.copy(error_text_style)
            misc.merge(style, @props.style)
        else
            style = error_text_style
        if @props.error?
            if typeof(@props.error) == 'string'
                error = @props.error
            else
                error = misc.to_json(@props.error)
        else
            error = @props.error_component
        bsStyle = @props.bsStyle ? 'danger'
        <Alert bsStyle={bsStyle} style={style}>
            {@render_close_button() if @props.onClose?}
            {@render_title() if @props.title}
            {error}
        </Alert>

exports.Footer = rclass
    displayName : "Footer"

    mixins: [ImmutablePureRenderMixin]

    render: ->
        <footer style={fontSize:"small",color:"gray",textAlign:"center",padding: "#{2*UNIT}px 0" }>
            <hr/>
            <Space/>
            <SiteName/> by <CompanyName/>
            {' '} &middot; {' '}
            <a target="_blank" href=PolicyIndexPageUrl>Policies</a>
            {' '} &middot; {' '}
            <a target="_blank" href=PolicyTOSPageUrl>Terms of Service</a>
            {' '} &middot; {' '}
            <HelpEmailLink />
            {' '} &middot; {' '}
            <span title="Version #{smc_version} @ #{build_date} | #{smc_git_rev[..8]}">&copy; {misc.YEAR}</span>
        </footer>


exports.MessageDisplay = MessageDisplay = rclass
    displayName : 'Misc-MessageDisplay'

    propTypes :
        message : rtypes.string
        onClose : rtypes.func

    render: ->
        <Row style={backgroundColor:'white', margin:'1ex', padding:'1ex', border:'1px solid lightgray', dropShadow:'3px 3px 3px lightgray', borderRadius:'3px'}>
            <Col md=8 xs=8>
                <span style={color:'gray', marginRight:'1ex'}>{@props.message}</span>
            </Col>
            <Col md=4 xs=4>
                <Button className='pull-right' onClick={@props.onClose} bsSize='small'>
                    <Icon name='times' />
                </Button>
            </Col>
        </Row>

exports.SelectorInput = SelectorInput = rclass
    displayName : 'Misc-SelectorInput'

    propTypes :
        selected  : rtypes.string
        on_change : rtypes.func
        disabled  : rtypes.bool
        #options   : array or object

    render_options: ->
        if misc.is_array(@props.options)
            if @props.options.length > 0 and typeof(@props.options[0]) == 'string'
                i = 0
                v = []
                for x in @props.options
                    v.push(<option key={i} value={x}>{x}</option>)
                    i += 1
                return v
            else
                for x in @props.options
                    <option key={x.value} value={x.value}>{x.display}</option>
        else
            v = misc.keys(@props.options); v.sort()
            for value in v
                display = @props.options[value]
                <option key={value} value={value}>{display}</option>

    render: ->
        <FormGroup>
            <FormControl
                value          = {@props.selected}
                componentClass = 'select'
                ref            = 'input'
                onChange       = {=>@props.on_change?(ReactDOM.findDOMNode(@refs.input).value)}
                disabled       = {@props.disabled}
            >
                {@render_options()}
            </FormControl>
        </FormGroup>

exports.TextInput = rclass
    displayName : 'Misc-TextInput'

    propTypes :
        text : rtypes.string.isRequired
        on_change : rtypes.func.isRequired
        type : rtypes.string
        rows : rtypes.number

    componentWillReceiveProps: (next_props) ->
        if @props.text != next_props.text
            # so when the props change the state stays in sync (e.g., so save button doesn't appear, etc.)
            @setState(text : next_props.text)

    getInitialState: ->
        text : @props.text

    saveChange: (event) ->
        event.preventDefault()
        @props.on_change(@state.text)

    render_save_button: ->
        if @state.text? and @state.text != @props.text
            <Button  style={marginBottom:'15px'} bsStyle='success' onClick={@saveChange}><Icon name='save' /> Save</Button>

    render_input: ->
        <FormGroup>
            <FormControl type={@props.type ? 'text'} ref='input' rows={@props.rows}
                       componentClass={if @props.type == 'textarea' then 'textarea' else 'input'}
                       value={if @state.text? then @state.text else @props.text}
                       onChange={=>@setState(text:ReactDOM.findDOMNode(@refs.input).value)}
            />
        </FormGroup>

    render: ->
        <form onSubmit={@saveChange}>
            {@render_input()}
            {@render_save_button()}
        </form>

exports.NumberInput = NumberInput = rclass
    displayName : 'Misc-NumberInput'

    propTypes :
        number      : rtypes.number.isRequired
        min         : rtypes.number.isRequired
        max         : rtypes.number.isRequired
        on_change   : rtypes.func.isRequired
        unit        : rtypes.string
        disabled    : rtypes.bool

    componentWillReceiveProps: (next_props) ->
        if @props.number != next_props.number
            # so when the props change the state stays in sync (e.g., so save button doesn't appear, etc.)
            @setState(number : next_props.number)

    getInitialState: ->
        number : @props.number

    saveChange: (e) ->
        e?.preventDefault()
        n = parseInt(@state.number)
        if "#{n}" == "NaN"
            n = @props.number
        if n < @props.min
            n = @props.min
        else if n > @props.max
            n = @props.max
        @setState(number:n)
        @props.on_change(n)

    render_save_button: ->
        if @state.number? and @state.number != @props.number
            <Button className='pull-right' bsStyle='success' onClick={@saveChange}><Icon name='save' /> Save</Button>

    render: ->
        unit = if @props.unit? then "#{@props.unit}" else ''
        <Row>
            <Col xs=6>
                <form onSubmit={@saveChange}>
                    <FormGroup>
                        <FormControl
                            type     = 'text'
                            ref      = 'input'
                            value    = {if @state.number? then @state.number else @props.number}
                            onChange = {=>@setState(number:ReactDOM.findDOMNode(@refs.input).value)}
                            onBlur   = {@saveChange}
                            onKeyDown= {(e)=>if e.keyCode == 27 then @setState(number:@props.number)}
                            disabled = {@props.disabled}
                        />
                    </FormGroup>
                </form>
            </Col>
            <Col xs=6 className="lighten">
                {unit}
            </Col>
        </Row>

exports.LabeledRow = LabeledRow = rclass
    displayName : 'Misc-LabeledRow'

    propTypes :
        label      : rtypes.any.isRequired
        style      : rtypes.object
        label_cols : rtypes.number    # number between 1 and 11 (default: 4)

    getDefaultProps: ->
        label_cols : 4

    render: ->
        <Row style={@props.style}>
            <Col xs={@props.label_cols}>
                {@props.label}
            </Col>
            <Col xs={12-@props.label_cols}>
                {@props.children}
            </Col>
        </Row>

help_text =
  backgroundColor: 'white'
  padding        : '10px'
  borderRadius   : '5px'
  margin         : '5px'

exports.Help = rclass
    displayName : 'Misc-Help'

    propTypes :
        button_label : rtypes.string.isRequired
        title        : rtypes.string.isRequired

    getDefaultProps: ->
        button_label : 'Help'
        title        : 'Help'

    getInitialState: ->
        closed : true

    render_title: ->
        <span>
            {@props.title}
        </span>

    render: ->
        if @state.closed
            <div>
                <Button bsStyle='info' onClick={=>@setState(closed:false)}><Icon name='question-circle'/> {@props.button_label}</Button>
            </div>
        else
            <Well style={width:500, zIndex:10, boxShadow:'3px 3px 3px #aaa', position:'absolute'} className='well'>
                <a href='' style={float:'right'} onClick={(e)=>e.preventDefault();@setState(closed:true)}><Icon name='times'/></a>
                <h4>{@props.title}
                </h4>
                <div style={help_text}>
                    {@props.children}
                </div>
            </Well>


###
# Customized TimeAgo support
# TODO: internationalize this formatter -- see https://www.npmjs.com/package/react-timeago
###

timeago_formatter = (value, unit, suffix, date) ->
    if value == 0
        return 'now'
    if unit == 'second'
        return "less than a minute #{suffix}"
    if value != 1
        unit += 's'
    return "#{value} #{unit} #{suffix}"

TimeAgo = require('react-timeago').default
exports.TimeAgo = rclass
    displayName : 'Misc-TimeAgo'

    propTypes :
        popover   : rtypes.bool
        placement : rtypes.string
        tip       : rtypes.string     # optional body of the tip popover with title the original time.

    getDefaultProps: ->
        popover   : true
        minPeriod : 45    # "minPeriod and maxPeriod now accept seconds not milliseconds. This matches the documentation."
        placement : 'top'
        # critical to use minPeriod>>1000, or things will get really slow in the client!!
        # Also, given our custom formatter, anything more than about 45s is pointless (since we don't show seconds)

    render_timeago: (d) ->
        <TimeAgo title='' date={d} style={@props.style} formatter={timeago_formatter} minPeriod={@props.minPeriod} />

    render: ->
        d = if misc.is_date(@props.date) then @props.date else new Date(@props.date)
        if isNaN(d)  # http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
            # This can and does happen!  Passing this to the third party TimeAgo component
            # goes ballistic and crashes react (lame, but whatever).
            return <div>Invalid Date</div>
        if @props.popover
            s = d.toLocaleString()
            <Tip title={s} tip={@props.tip} id={s} placement={@props.placement}>
                {@render_timeago(d)}
            </Tip>
        else
            @render_timeago(d)


# Important:
# widget can be controlled or uncontrolled -- use default_value for an *uncontrolled* widget
# with callbacks, and value for a controlled one!
#    See http://facebook.github.io/react/docs/forms.html#controlled-components

# Search input box with the following capabilities
# a clear button (that focuses the input)
# `enter` to submit
# `esc` to clear
exports.SearchInput = rclass
    displayName : 'Misc-SearchInput'

    propTypes :
        autoFocus       : rtypes.bool
        autoSelect      : rtypes.bool
        placeholder     : rtypes.string
        default_value   : rtypes.string
        value           : rtypes.string
        on_change       : rtypes.func    # invoked as on_change(value, get_opts()) each time the search input changes
        on_submit       : rtypes.func    # invoked as on_submit(value, get_opts()) when the search input is submitted (by hitting enter)
        on_escape       : rtypes.func    # invoked when user presses escape key; on_escape(value *before* hitting escape)
        on_up           : rtypes.func    # push up arrow
        on_down         : rtypes.func    # push down arrow
        on_clear        : rtypes.func    # invoked without arguments when input box is cleared (eg. via esc or clicking the clear button)
        clear_on_submit : rtypes.bool    # if true, will clear search box on every submit (default: false)
        buttonAfter     : rtypes.object

    getInitialState: ->
        value     : (@props.value || @props.default_value) ? ''
        ctrl_down : false

    get_opts: ->
        ctrl_down : @state.ctrl_down

    componentWillReceiveProps: (new_props) ->
        if new_props.value?
            @setState(value : new_props.value)

    componentDidMount: ->
        if @props.autoSelect
            try
                ReactDOM.findDOMNode(@refs.input).select()
            catch e
                # Edge sometimes complains about 'Could not complete the operation due to error 800a025e'

    clear_value: ->
        @set_value('')
        @props.on_clear?()

    clear_and_focus_search_input: ->
        @clear_value()
        ReactDOM.findDOMNode(@refs.input).focus()

    search_button: ->
        if @props.buttonAfter?
            return @props.buttonAfter
        else
            s = if @state.value?.length > 0 then 'warning' else "default"
            <Button onClick={@clear_and_focus_search_input} bsStyle={s}>
                <Icon name='times-circle' />
            </Button>

    set_value: (value) ->
        @setState(value:value)
        @props.on_change?(value, @get_opts())

    submit: (e) ->
        e?.preventDefault()
        @props.on_submit?(@state.value, @get_opts())
        if @props.clear_on_submit
            @clear_value()
            @props.on_change?(@state.value, @get_opts())

    key_down: (e) ->
        switch e.keyCode
            when 27
                @escape()
            when 40
                @props.on_down?()
            when 38
                @props.on_up?()
            when 17
                @setState(ctrl_down : true)
            when 13
                @submit()

    key_up: (e) ->
        switch e.keyCode
            when 17
                @setState(ctrl_down : false)

    escape: ->
        @props.on_escape?(@state.value)
        @clear_value()

    render: ->
        <FormGroup>
            <InputGroup>
                <FormControl
                    autoFocus   = {@props.autoFocus}
                    ref         = 'input'
                    type        = 'text'
                    placeholder = {@props.placeholder}
                    value       = {@state.value}
                    onChange    = {=>@set_value(ReactDOM.findDOMNode(@refs.input).value)}
                    onKeyDown   = {@key_down}
                    onKeyUp     = {@key_up}
                />
                <InputGroup.Button>
                    {@search_button()}
                </InputGroup.Button>
            </InputGroup>
        </FormGroup>

exports.MarkdownInput = rclass
    displayName : 'Misc-MarkdownInput'

    propTypes :
        default_value : rtypes.string
        on_change     : rtypes.func
        on_save       : rtypes.func   # called when saving from editing and switching back
        on_edit       : rtypes.func   # called when editing starts
        on_cancel     : rtypes.func   # called when cancel button clicked
        rows          : rtypes.number
        placeholder   : rtypes.string

    getInitialState: ->
        editing : false
        value   : undefined

    edit: ->
        @props.on_edit?()
        @setState(value:@props.default_value ? '', editing:true)

    cancel: ->
        @props.on_cancel?()
        @setState(editing:false)

    save: ->
        @props.on_save?(@state.value)
        @setState(editing:false)

    keydown: (e) ->
        if e.keyCode==27
            @setState(editing:false)
        else if e.keyCode==13 and e.shiftKey
            @save()

    to_html: ->
        if @props.default_value
            {__html: markdown.markdown_to_html(@props.default_value).s}
        else
            {__html: ''}

    render: ->
        if @state.editing

            tip = <span>
                You may enter (Github flavored) markdown here.  In particular, use # for headings, > for block quotes, *'s for italic text, **'s for bold text, - at the beginning of a line for lists, back ticks ` for code, and URL's will automatically become links.
            </span>

            <div>
                <ButtonToolbar style={paddingBottom:'5px'}>
                    <Button key='save' bsStyle='success' onClick={@save}
                            disabled={@state.value == @props.default_value}>
                        <Icon name='edit' /> Save
                    </Button>
                    <Button key='cancel' onClick={@cancel}>Cancel</Button>
                </ButtonToolbar>
                <form onSubmit={@save} style={marginBottom: '-20px'}>
                    <FormGroup>
                        <FormControl autoFocus
                            ref         = 'input'
                            componentClass = 'textarea'
                            rows        = {@props.rows ? 4}
                            placeholder = {@props.placeholder}
                            value       = {@state.value}
                            onChange    = {=>x=ReactDOM.findDOMNode(@refs.input).value; @setState(value:x); @props.on_change?(x)}
                            onKeyDown   = {@keydown}
                        />
                    </FormGroup>
                </form>
                <div style={paddingTop:'8px', color:'#666'}>
                    <Tip title='Use Markdown' tip={tip}>
                        Format using <a href='https://help.github.com/articles/getting-started-with-writing-and-formatting-on-github/' target='_blank'>Markdown</a>
                    </Tip>
                </div>
            </div>
        else
            <div>
                {<Button onClick={@edit}>Edit</Button>}
                <div onClick={@edit} dangerouslySetInnerHTML={@to_html()}></div>
            </div>

exports.HTML = rclass
    displayName : 'Misc-HTML'

    propTypes :
        value          : rtypes.string
        style          : rtypes.object
        has_mathjax    : rtypes.bool
        project_id     : rtypes.string   # optional -- can be used to improve link handling (e.g., to images)
        file_path      : rtypes.string   # optional -- ...
        className      : rtypes.string   # optional class
        safeHTML       : rtypes.bool     # optional -- default true, if true scripts and unsafe attributes are removed from sanitized html
        href_transform : rtypes.func     # optional function that link/src hrefs are fed through
        post_hook      : rtypes.func     # optional function post_hook(elt), which should mutate elt, where elt is
                                         # the jQuery wrapped set that is created (and discarded!) in the course of
                                         # sanitizing input.  Use this as an opportunity to modify the HTML structure
                                         # before it is exported to text and given to react.   Obviously, you can't
                                         # install click handlers here.

    getDefaultProps: ->
        has_mathjax : true
        safeHTML    : true

    shouldComponentUpdate: (newProps) ->
        return @props.value != newProps.value or \
             not underscore.isEqual(@props.style, newProps.style) or \
             @props.safeHTML != newProps.safeHTML

    _update_escaped_chars: ->
        if not @_is_mounted
            return
        node = $(ReactDOM.findDOMNode(@))
        node.html(node[0].innerHTML.replace(/\\\$/g, '$'))

    _update_mathjax: (cb) ->
        if not @_is_mounted  # see https://github.com/sagemathinc/smc/issues/1689
            cb()
            return
        if @props.has_mathjax
            $(ReactDOM.findDOMNode(@)).mathjax
                cb : () =>
                    # Awkward code, since cb may be called more than once if there
                    # where more than one node.
                    cb?()
                    cb = undefined
        else
            cb()

    _update_links: ->
        if not @_is_mounted
            return
        $(ReactDOM.findDOMNode(@)).process_smc_links
            project_id     : @props.project_id
            file_path      : @props.file_path
            href_transform : @props.href_transform

    _update_tables: ->
        if not @_is_mounted
            return
        $(ReactDOM.findDOMNode(@)).find("table").addClass('table')

    update_content: ->
        if not @_is_mounted
            return
        # orchestrates the _update_* methods
        @_update_mathjax =>
            if not @_is_mounted
                return
            @_update_escaped_chars()
            @_update_links()   # this MUST be after update_escaped_chars -- see https://github.com/sagemathinc/smc/issues/1391
            @_update_tables()

    componentDidUpdate: ->
        @update_content()

    componentDidMount: ->
        @_is_mounted = true
        @update_content()

    componentWillUnmount: ->
        @_is_mounted = false

    render_html: ->
        if @props.value
            if @props.safeHTML
                html = require('./misc_page').sanitize_html_safe(@props.value, @props.post_hook)
            else
                html = require('./misc_page').sanitize_html(@props.value, true, true, @props.post_hook)
            {__html: html}
        else
            {__html: ''}

    render: ->
        <span
            className               = {@props.className}
            dangerouslySetInnerHTML = {@render_html()}
            style                   = {@props.style}>
        </span>
<commit_msg>fix https://github.com/sagemathinc/smc/issues/1895 -- "display of mathjax in safari (in markdown output) is completely broken"<commit_after>###############################################################################
#
# SageMathCloud: A collaborative web-based interface to Sage, IPython, LaTeX and the Terminal.
#
#    Copyright (C) 2016, Sagemath Inc.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

async = require('async')

{React, ReactDOM, rclass, rtypes, is_redux, is_redux_actions, redux} = require('./smc-react')
{Alert, Button, ButtonToolbar, Checkbox, Col, FormControl, FormGroup, ControlLabel, InputGroup, OverlayTrigger, Popover, Tooltip, Row, Well} = require('react-bootstrap')
{HelpEmailLink, SiteName, CompanyName, PricingUrl, PolicyTOSPageUrl, PolicyIndexPageUrl, PolicyPricingPageUrl} = require('./customize')

# injected by webpack, but not for react-static renderings (ATTN don't assign to uppercase vars!)
smc_version = SMC_VERSION ? 'N/A'
build_date  = BUILD_DATE  ? 'N/A'
smc_git_rev = SMC_GIT_REV ? 'N/A'

Combobox    = require('react-widgets/lib/Combobox')

misc        = require('smc-util/misc')
immutable   = require('immutable')
underscore  = require('underscore')

markdown    = require('./markdown')

{defaults, required} = misc

# base unit in pixel for margin/size/padding
exports.UNIT = UNIT = 15

# bootstrap blue background
exports.BS_BLUE_BGRND = "rgb(66, 139, 202)"

exports.SAGE_LOGO_COLOR = exports.BS_BLUE_BGRND

# Checks whether two immutable variables (either ImmutableJS objects or actual
# immutable types) are equal. Gives a warning and returns false (no matter what) if either variable is mutable.
immutable_equals_single = (a, b) ->
    if typeof(a) == "object" or typeof(b) == "object"
        if (is_redux(a) and is_redux(b)) or (is_redux_actions(a) and is_redux_actions(b))
            return a == b
        if immutable.Iterable.isIterable(a) and immutable.Iterable.isIterable(b)
            return immutable.is(a, b)
        if (a? and not b?) or (not a? and b?)
            # if one is undefined and the other is defined, they aren't equal
            return false
        console.warn("Using mutable object in ImmutablePureRenderMixin:", a, b)
        return false
    return a == b

immutable_equals = (objA, objB) ->
    if immutable.is(objA, objB)
        return true
    keysA = misc.keys(objA)
    keysB = misc.keys(objB)
    if keysA.length != keysB.length
        return false

    for key in keysA
        if not objB.hasOwnProperty(key) or not immutable_equals_single(objA[key], objB[key])
            return false
    return true

# Like PureRenderMixin, except only for immutable variables. Will always
# re-render if any props are mutable objects.
exports.ImmutablePureRenderMixin = ImmutablePureRenderMixin =
    shouldComponentUpdate: (nextProps, nextState) ->
        not immutable_equals(@props, nextProps) or not immutable_equals(@state, nextState)

# Gives components a setInterval method that takes a function and time x milliseconds
# then calls that function every x milliseconds. Automatically stops calling
# when component is unmounted. Can be called multiple times for multiple intervals.
exports.SetIntervalMixin =
    componentWillMount: ->
        @intervals = []
    setInterval: (fn, ms) ->
        @intervals.push setInterval fn, ms
    componentWillUnmount: ->
        @intervals.forEach clearInterval

exports.Space = Space = ->
    <span>&nbsp</span>

# Font Awesome component -- obviously TODO move to own file
# Converted from https://github.com/andreypopp/react-fa
exports.Icon = Icon = rclass
    displayName : 'Icon'

    propTypes :
        name       : rtypes.string
        size       : rtypes.oneOf(['lg', '2x', '3x', '4x', '5x'])
        rotate     : rtypes.oneOf(['45', '90', '135', '180', '225', '270', '315'])
        flip       : rtypes.oneOf(['horizontal', 'vertical'])
        spin       : rtypes.bool
        pulse      : rtypes.bool
        fixedWidth : rtypes.bool
        stack      : rtypes.oneOf(['1x', '2x'])
        inverse    : rtypes.bool
        className  : rtypes.string
        style      : rtypes.object
        onClick    : rtypes.func
        onMouseOver: rtypes.func
        onMouseOut : rtypes.func

    getDefaultProps: ->
        name    : 'square-o'
        onClick : ->

    render: ->
        {name, size, rotate, flip, spin, pulse, fixedWidth, stack, inverse, className, style} = @props
        # temporary until file_associations can be changed
        if name.slice(0, 3) == 'fa-'
            classNames = "fa #{name}"
        else
            classNames = "fa fa-#{name}"
        if size
            classNames += " fa-#{size}"
        if rotate
            classNames += " fa-rotate-#{rotate}"
        if flip
            classNames += " fa-flip-#{flip}"
        if fixedWidth
            classNames += ' fa-fw'
        if spin
            classNames += ' fa-spin'
        if pulse
            classNames += ' fa-pulse'
        if stack
            classNames += " fa-stack-#{stack}"
        if inverse
            classNames += ' fa-inverse'
        if className
            classNames += " #{className}"
        return <i style={style} className={classNames} onMouseOver={@props.onMouseOver} onMouseOut={@props.onMouseOut} onClick={@props.onClick}>{@props.children}</i>

# this Octicon icon class requires the CSS file in octicons/octicons/octicons.css (see landing.coffee)
exports.Octicon = rclass
    displayName : 'Octicon'

    propTypes :
        name   : rtypes.string.isRequired
        mega   : rtypes.bool
        spin   : rtypes.bool

    getDefaultProps: ->
        name : 'flame'
        mega : false
        spin : false

    render: ->
        classNames = ['octicon', "octicon-#{@props.name}"]
        if @props.spin
            classNames.push('spin-octicon')
        if @props.mega
            classNames.push('mega-octicon')
        return <span className={classNames.join(' ')} />

exports.Loading = Loading = rclass
    displayName : 'Misc-Loading'

    propTypes :
        style : rtypes.object

    render: ->
        <span style={@props.style}>
            <Icon name='circle-o-notch' spin /> Loading...
        </span>

exports.Saving = Saving = rclass
    displayName : 'Misc-Saving'

    render: ->
        <span><Icon name='circle-o-notch' spin /> Saving...</span>

closex_style =
    float      : 'right'
    marginLeft : '5px'

exports.CloseX = CloseX = rclass
    displayName : 'Misc-CloseX'

    propTypes :
        on_close : rtypes.func.isRequired
        style    : rtypes.object   # optional style for the icon itself

    render:->
        <a href='' style={closex_style} onClick={(e)=>e.preventDefault();@props.on_close()}>
            <Icon style={@props.style} name='times' />
        </a>


error_text_style =
    marginRight : '1ex'
    whiteSpace  : 'pre-line'
    maxWidth    : '80ex'

exports.ErrorDisplay = ErrorDisplay = rclass
    displayName : 'Misc-ErrorDisplay'

    propTypes :
        error   : rtypes.oneOfType([rtypes.string,rtypes.object])
        error_component : rtypes.any
        title   : rtypes.string
        style   : rtypes.object
        bsStyle : rtypes.string
        onClose : rtypes.func       # TODO: change to on_close everywhere...?

    render_close_button: ->
        <CloseX on_close={@props.onClose} style={fontSize:'11pt'} />

    render_title: ->
        <h4>{@props.title}</h4>

    render: ->
        if @props.style?
            style = misc.copy(error_text_style)
            misc.merge(style, @props.style)
        else
            style = error_text_style
        if @props.error?
            if typeof(@props.error) == 'string'
                error = @props.error
            else
                error = misc.to_json(@props.error)
        else
            error = @props.error_component
        bsStyle = @props.bsStyle ? 'danger'
        <Alert bsStyle={bsStyle} style={style}>
            {@render_close_button() if @props.onClose?}
            {@render_title() if @props.title}
            {error}
        </Alert>

exports.Footer = rclass
    displayName : "Footer"

    mixins: [ImmutablePureRenderMixin]

    render: ->
        <footer style={fontSize:"small",color:"gray",textAlign:"center",padding: "#{2*UNIT}px 0" }>
            <hr/>
            <Space/>
            <SiteName/> by <CompanyName/>
            {' '} &middot; {' '}
            <a target="_blank" href=PolicyIndexPageUrl>Policies</a>
            {' '} &middot; {' '}
            <a target="_blank" href=PolicyTOSPageUrl>Terms of Service</a>
            {' '} &middot; {' '}
            <HelpEmailLink />
            {' '} &middot; {' '}
            <span title="Version #{smc_version} @ #{build_date} | #{smc_git_rev[..8]}">&copy; {misc.YEAR}</span>
        </footer>


exports.MessageDisplay = MessageDisplay = rclass
    displayName : 'Misc-MessageDisplay'

    propTypes :
        message : rtypes.string
        onClose : rtypes.func

    render: ->
        <Row style={backgroundColor:'white', margin:'1ex', padding:'1ex', border:'1px solid lightgray', dropShadow:'3px 3px 3px lightgray', borderRadius:'3px'}>
            <Col md=8 xs=8>
                <span style={color:'gray', marginRight:'1ex'}>{@props.message}</span>
            </Col>
            <Col md=4 xs=4>
                <Button className='pull-right' onClick={@props.onClose} bsSize='small'>
                    <Icon name='times' />
                </Button>
            </Col>
        </Row>

exports.SelectorInput = SelectorInput = rclass
    displayName : 'Misc-SelectorInput'

    propTypes :
        selected  : rtypes.string
        on_change : rtypes.func
        disabled  : rtypes.bool
        #options   : array or object

    render_options: ->
        if misc.is_array(@props.options)
            if @props.options.length > 0 and typeof(@props.options[0]) == 'string'
                i = 0
                v = []
                for x in @props.options
                    v.push(<option key={i} value={x}>{x}</option>)
                    i += 1
                return v
            else
                for x in @props.options
                    <option key={x.value} value={x.value}>{x.display}</option>
        else
            v = misc.keys(@props.options); v.sort()
            for value in v
                display = @props.options[value]
                <option key={value} value={value}>{display}</option>

    render: ->
        <FormGroup>
            <FormControl
                value          = {@props.selected}
                componentClass = 'select'
                ref            = 'input'
                onChange       = {=>@props.on_change?(ReactDOM.findDOMNode(@refs.input).value)}
                disabled       = {@props.disabled}
            >
                {@render_options()}
            </FormControl>
        </FormGroup>

exports.TextInput = rclass
    displayName : 'Misc-TextInput'

    propTypes :
        text : rtypes.string.isRequired
        on_change : rtypes.func.isRequired
        type : rtypes.string
        rows : rtypes.number

    componentWillReceiveProps: (next_props) ->
        if @props.text != next_props.text
            # so when the props change the state stays in sync (e.g., so save button doesn't appear, etc.)
            @setState(text : next_props.text)

    getInitialState: ->
        text : @props.text

    saveChange: (event) ->
        event.preventDefault()
        @props.on_change(@state.text)

    render_save_button: ->
        if @state.text? and @state.text != @props.text
            <Button  style={marginBottom:'15px'} bsStyle='success' onClick={@saveChange}><Icon name='save' /> Save</Button>

    render_input: ->
        <FormGroup>
            <FormControl type={@props.type ? 'text'} ref='input' rows={@props.rows}
                       componentClass={if @props.type == 'textarea' then 'textarea' else 'input'}
                       value={if @state.text? then @state.text else @props.text}
                       onChange={=>@setState(text:ReactDOM.findDOMNode(@refs.input).value)}
            />
        </FormGroup>

    render: ->
        <form onSubmit={@saveChange}>
            {@render_input()}
            {@render_save_button()}
        </form>

exports.NumberInput = NumberInput = rclass
    displayName : 'Misc-NumberInput'

    propTypes :
        number      : rtypes.number.isRequired
        min         : rtypes.number.isRequired
        max         : rtypes.number.isRequired
        on_change   : rtypes.func.isRequired
        unit        : rtypes.string
        disabled    : rtypes.bool

    componentWillReceiveProps: (next_props) ->
        if @props.number != next_props.number
            # so when the props change the state stays in sync (e.g., so save button doesn't appear, etc.)
            @setState(number : next_props.number)

    getInitialState: ->
        number : @props.number

    saveChange: (e) ->
        e?.preventDefault()
        n = parseInt(@state.number)
        if "#{n}" == "NaN"
            n = @props.number
        if n < @props.min
            n = @props.min
        else if n > @props.max
            n = @props.max
        @setState(number:n)
        @props.on_change(n)

    render_save_button: ->
        if @state.number? and @state.number != @props.number
            <Button className='pull-right' bsStyle='success' onClick={@saveChange}><Icon name='save' /> Save</Button>

    render: ->
        unit = if @props.unit? then "#{@props.unit}" else ''
        <Row>
            <Col xs=6>
                <form onSubmit={@saveChange}>
                    <FormGroup>
                        <FormControl
                            type     = 'text'
                            ref      = 'input'
                            value    = {if @state.number? then @state.number else @props.number}
                            onChange = {=>@setState(number:ReactDOM.findDOMNode(@refs.input).value)}
                            onBlur   = {@saveChange}
                            onKeyDown= {(e)=>if e.keyCode == 27 then @setState(number:@props.number)}
                            disabled = {@props.disabled}
                        />
                    </FormGroup>
                </form>
            </Col>
            <Col xs=6 className="lighten">
                {unit}
            </Col>
        </Row>

exports.LabeledRow = LabeledRow = rclass
    displayName : 'Misc-LabeledRow'

    propTypes :
        label      : rtypes.any.isRequired
        style      : rtypes.object
        label_cols : rtypes.number    # number between 1 and 11 (default: 4)

    getDefaultProps: ->
        label_cols : 4

    render: ->
        <Row style={@props.style}>
            <Col xs={@props.label_cols}>
                {@props.label}
            </Col>
            <Col xs={12-@props.label_cols}>
                {@props.children}
            </Col>
        </Row>

help_text =
  backgroundColor: 'white'
  padding        : '10px'
  borderRadius   : '5px'
  margin         : '5px'

exports.Help = rclass
    displayName : 'Misc-Help'

    propTypes :
        button_label : rtypes.string.isRequired
        title        : rtypes.string.isRequired

    getDefaultProps: ->
        button_label : 'Help'
        title        : 'Help'

    getInitialState: ->
        closed : true

    render_title: ->
        <span>
            {@props.title}
        </span>

    render: ->
        if @state.closed
            <div>
                <Button bsStyle='info' onClick={=>@setState(closed:false)}><Icon name='question-circle'/> {@props.button_label}</Button>
            </div>
        else
            <Well style={width:500, zIndex:10, boxShadow:'3px 3px 3px #aaa', position:'absolute'} className='well'>
                <a href='' style={float:'right'} onClick={(e)=>e.preventDefault();@setState(closed:true)}><Icon name='times'/></a>
                <h4>{@props.title}
                </h4>
                <div style={help_text}>
                    {@props.children}
                </div>
            </Well>


###
# Customized TimeAgo support
# TODO: internationalize this formatter -- see https://www.npmjs.com/package/react-timeago
###

timeago_formatter = (value, unit, suffix, date) ->
    if value == 0
        return 'now'
    if unit == 'second'
        return "less than a minute #{suffix}"
    if value != 1
        unit += 's'
    return "#{value} #{unit} #{suffix}"

TimeAgo = require('react-timeago').default
exports.TimeAgo = rclass
    displayName : 'Misc-TimeAgo'

    propTypes :
        popover   : rtypes.bool
        placement : rtypes.string
        tip       : rtypes.string     # optional body of the tip popover with title the original time.

    getDefaultProps: ->
        popover   : true
        minPeriod : 45    # "minPeriod and maxPeriod now accept seconds not milliseconds. This matches the documentation."
        placement : 'top'
        # critical to use minPeriod>>1000, or things will get really slow in the client!!
        # Also, given our custom formatter, anything more than about 45s is pointless (since we don't show seconds)

    render_timeago: (d) ->
        <TimeAgo title='' date={d} style={@props.style} formatter={timeago_formatter} minPeriod={@props.minPeriod} />

    render: ->
        d = if misc.is_date(@props.date) then @props.date else new Date(@props.date)
        if isNaN(d)  # http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
            # This can and does happen!  Passing this to the third party TimeAgo component
            # goes ballistic and crashes react (lame, but whatever).
            return <div>Invalid Date</div>
        if @props.popover
            s = d.toLocaleString()
            <Tip title={s} tip={@props.tip} id={s} placement={@props.placement}>
                {@render_timeago(d)}
            </Tip>
        else
            @render_timeago(d)


# Important:
# widget can be controlled or uncontrolled -- use default_value for an *uncontrolled* widget
# with callbacks, and value for a controlled one!
#    See http://facebook.github.io/react/docs/forms.html#controlled-components

# Search input box with the following capabilities
# a clear button (that focuses the input)
# `enter` to submit
# `esc` to clear
exports.SearchInput = rclass
    displayName : 'Misc-SearchInput'

    propTypes :
        autoFocus       : rtypes.bool
        autoSelect      : rtypes.bool
        placeholder     : rtypes.string
        default_value   : rtypes.string
        value           : rtypes.string
        on_change       : rtypes.func    # invoked as on_change(value, get_opts()) each time the search input changes
        on_submit       : rtypes.func    # invoked as on_submit(value, get_opts()) when the search input is submitted (by hitting enter)
        on_escape       : rtypes.func    # invoked when user presses escape key; on_escape(value *before* hitting escape)
        on_up           : rtypes.func    # push up arrow
        on_down         : rtypes.func    # push down arrow
        on_clear        : rtypes.func    # invoked without arguments when input box is cleared (eg. via esc or clicking the clear button)
        clear_on_submit : rtypes.bool    # if true, will clear search box on every submit (default: false)
        buttonAfter     : rtypes.object

    getInitialState: ->
        value     : (@props.value || @props.default_value) ? ''
        ctrl_down : false

    get_opts: ->
        ctrl_down : @state.ctrl_down

    componentWillReceiveProps: (new_props) ->
        if new_props.value?
            @setState(value : new_props.value)

    componentDidMount: ->
        if @props.autoSelect
            try
                ReactDOM.findDOMNode(@refs.input).select()
            catch e
                # Edge sometimes complains about 'Could not complete the operation due to error 800a025e'

    clear_value: ->
        @set_value('')
        @props.on_clear?()

    clear_and_focus_search_input: ->
        @clear_value()
        ReactDOM.findDOMNode(@refs.input).focus()

    search_button: ->
        if @props.buttonAfter?
            return @props.buttonAfter
        else
            s = if @state.value?.length > 0 then 'warning' else "default"
            <Button onClick={@clear_and_focus_search_input} bsStyle={s}>
                <Icon name='times-circle' />
            </Button>

    set_value: (value) ->
        @setState(value:value)
        @props.on_change?(value, @get_opts())

    submit: (e) ->
        e?.preventDefault()
        @props.on_submit?(@state.value, @get_opts())
        if @props.clear_on_submit
            @clear_value()
            @props.on_change?(@state.value, @get_opts())

    key_down: (e) ->
        switch e.keyCode
            when 27
                @escape()
            when 40
                @props.on_down?()
            when 38
                @props.on_up?()
            when 17
                @setState(ctrl_down : true)
            when 13
                @submit()

    key_up: (e) ->
        switch e.keyCode
            when 17
                @setState(ctrl_down : false)

    escape: ->
        @props.on_escape?(@state.value)
        @clear_value()

    render: ->
        <FormGroup>
            <InputGroup>
                <FormControl
                    autoFocus   = {@props.autoFocus}
                    ref         = 'input'
                    type        = 'text'
                    placeholder = {@props.placeholder}
                    value       = {@state.value}
                    onChange    = {=>@set_value(ReactDOM.findDOMNode(@refs.input).value)}
                    onKeyDown   = {@key_down}
                    onKeyUp     = {@key_up}
                />
                <InputGroup.Button>
                    {@search_button()}
                </InputGroup.Button>
            </InputGroup>
        </FormGroup>

exports.MarkdownInput = rclass
    displayName : 'Misc-MarkdownInput'

    propTypes :
        default_value : rtypes.string
        on_change     : rtypes.func
        on_save       : rtypes.func   # called when saving from editing and switching back
        on_edit       : rtypes.func   # called when editing starts
        on_cancel     : rtypes.func   # called when cancel button clicked
        rows          : rtypes.number
        placeholder   : rtypes.string

    getInitialState: ->
        editing : false
        value   : undefined

    edit: ->
        @props.on_edit?()
        @setState(value:@props.default_value ? '', editing:true)

    cancel: ->
        @props.on_cancel?()
        @setState(editing:false)

    save: ->
        @props.on_save?(@state.value)
        @setState(editing:false)

    keydown: (e) ->
        if e.keyCode==27
            @setState(editing:false)
        else if e.keyCode==13 and e.shiftKey
            @save()

    to_html: ->
        if @props.default_value
            {__html: markdown.markdown_to_html(@props.default_value).s}
        else
            {__html: ''}

    render: ->
        if @state.editing

            tip = <span>
                You may enter (Github flavored) markdown here.  In particular, use # for headings, > for block quotes, *'s for italic text, **'s for bold text, - at the beginning of a line for lists, back ticks ` for code, and URL's will automatically become links.
            </span>

            <div>
                <ButtonToolbar style={paddingBottom:'5px'}>
                    <Button key='save' bsStyle='success' onClick={@save}
                            disabled={@state.value == @props.default_value}>
                        <Icon name='edit' /> Save
                    </Button>
                    <Button key='cancel' onClick={@cancel}>Cancel</Button>
                </ButtonToolbar>
                <form onSubmit={@save} style={marginBottom: '-20px'}>
                    <FormGroup>
                        <FormControl autoFocus
                            ref         = 'input'
                            componentClass = 'textarea'
                            rows        = {@props.rows ? 4}
                            placeholder = {@props.placeholder}
                            value       = {@state.value}
                            onChange    = {=>x=ReactDOM.findDOMNode(@refs.input).value; @setState(value:x); @props.on_change?(x)}
                            onKeyDown   = {@keydown}
                        />
                    </FormGroup>
                </form>
                <div style={paddingTop:'8px', color:'#666'}>
                    <Tip title='Use Markdown' tip={tip}>
                        Format using <a href='https://help.github.com/articles/getting-started-with-writing-and-formatting-on-github/' target='_blank'>Markdown</a>
                    </Tip>
                </div>
            </div>
        else
            <div>
                {<Button onClick={@edit}>Edit</Button>}
                <div onClick={@edit} dangerouslySetInnerHTML={@to_html()}></div>
            </div>

exports.HTML = rclass
    displayName : 'Misc-HTML'

    propTypes :
        value          : rtypes.string
        style          : rtypes.object
        has_mathjax    : rtypes.bool
        project_id     : rtypes.string   # optional -- can be used to improve link handling (e.g., to images)
        file_path      : rtypes.string   # optional -- ...
        className      : rtypes.string   # optional class
        safeHTML       : rtypes.bool     # optional -- default true, if true scripts and unsafe attributes are removed from sanitized html
        href_transform : rtypes.func     # optional function that link/src hrefs are fed through
        post_hook      : rtypes.func     # optional function post_hook(elt), which should mutate elt, where elt is
                                         # the jQuery wrapped set that is created (and discarded!) in the course of
                                         # sanitizing input.  Use this as an opportunity to modify the HTML structure
                                         # before it is exported to text and given to react.   Obviously, you can't
                                         # install click handlers here.

    getDefaultProps: ->
        has_mathjax : true
        safeHTML    : true

    shouldComponentUpdate: (newProps) ->
        return @props.value != newProps.value or \
             not underscore.isEqual(@props.style, newProps.style) or \
             @props.safeHTML != newProps.safeHTML

    ###
    # Seems no longer necessary and *DOES* break massively on Safari! -- see https://github.com/sagemathinc/smc/issues/1895
    _update_escaped_chars: ->
        if not @_is_mounted
            return
        node = $(ReactDOM.findDOMNode(@))
        node.html(node[0].innerHTML.replace(/\\\$/g, '$'))
    ###

    _update_mathjax: (cb) ->
        if not @_is_mounted  # see https://github.com/sagemathinc/smc/issues/1689
            cb()
            return
        if @props.has_mathjax
            $(ReactDOM.findDOMNode(@)).mathjax
                cb : () =>
                    # Awkward code, since cb may be called more than once if there
                    # where more than one node.
                    cb?()
                    cb = undefined
        else
            cb()

    _update_links: ->
        if not @_is_mounted
            return
        $(ReactDOM.findDOMNode(@)).process_smc_links
            project_id     : @props.project_id
            file_path      : @props.file_path
            href_transform : @props.href_transform

    _update_tables: ->
        if not @_is_mounted
            return
        $(ReactDOM.findDOMNode(@)).find("table").addClass('table')

    update_content: ->
        if not @_is_mounted
            return
        # orchestrates the _update_* methods
        @_update_mathjax =>
            if not @_is_mounted
                return
            #@_update_escaped_chars()
            @_update_links()   # this MUST be after update_escaped_chars -- see https://github.com/sagemathinc/smc/issues/1391
            @_update_tables()

    componentDidUpdate: ->
        @update_content()

    componentDidMount: ->
        @_is_mounted = true
        @update_content()

    componentWillUnmount: ->
        @_is_mounted = false

    render_html: ->
        if @props.value
            if @props.safeHTML
                html = require('./misc_page').sanitize_html_safe(@props.value, @props.post_hook)
            else
                html = require('./misc_page').sanitize_html(@props.value, true, true, @props.post_hook)
            {__html: html}
        else
            {__html: ''}

    render: ->
        <span
            className               = {@props.className}
            dangerouslySetInnerHTML = {@render_html()}
            style                   = {@props.style}>
        </span>

{'fraction_non_alphanumeric': 0.11766007420771352, 'fraction_numerical': 0.00534431698267352, 'mean_word_length': 1.8496944417499273, 'pattern_counts': {'":': 0, '<': 291, '<?xml version=': 0, '>': 473, 'https://': 12, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 22, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '744149', 'n_tokens_mistral': 17047, 'n_tokens_neox': 16387, 'n_words': 4090}