import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react' import PropTypes from 'prop-types' import useScopeValue from '../hooks/use-scope-value' import usePersistedState from '../hooks/use-persisted-state' import useAbortController from '../hooks/use-abort-controller' import DocumentCompiler from '../../features/pdf-preview/util/compiler' import { send, sendMB, sendMBSampled, } from '../../infrastructure/event-tracking' import { buildLogEntryAnnotations, handleOutputFiles, } from '../../features/pdf-preview/util/output-files' import { useIdeContext } from './ide-context' import { useProjectContext } from './project-context' import { useEditorContext } from './editor-context' export const CompileContext = createContext() CompileContext.Provider.propTypes = { value: PropTypes.shape({ autoCompile: PropTypes.bool.isRequired, clearingCache: PropTypes.bool.isRequired, clsiServerId: PropTypes.string, codeCheckFailed: PropTypes.bool.isRequired, compiling: PropTypes.bool.isRequired, draft: PropTypes.bool.isRequired, error: PropTypes.string, fileList: PropTypes.object, hasChanges: PropTypes.bool.isRequired, hasLintingError: PropTypes.bool, logEntries: PropTypes.object, logEntryAnnotations: PropTypes.object, pdfDownloadUrl: PropTypes.string, pdfUrl: PropTypes.string, rawLog: PropTypes.string, setAutoCompile: PropTypes.func.isRequired, setDraft: PropTypes.func.isRequired, setError: PropTypes.func.isRequired, setHasLintingError: PropTypes.func.isRequired, // only for storybook setShowLogs: PropTypes.func.isRequired, setStopOnValidationError: PropTypes.func.isRequired, showLogs: PropTypes.bool.isRequired, stopOnValidationError: PropTypes.bool.isRequired, uncompiled: PropTypes.bool, validationIssues: PropTypes.object, firstRenderDone: PropTypes.func, }), } export function CompileProvider({ children }) { const ide = useIdeContext() const { hasPremiumCompile, isProjectOwner } = useEditorContext() const project = useProjectContext() const projectId = project._id // whether a compile is in progress const [compiling, setCompiling] = useState(false) // the log entries parsed from the compile output log const [logEntries, setLogEntries] = useScopeValue('pdf.logEntries') // annotations for display in the editor, built from the log entries const [logEntryAnnotations, setLogEntryAnnotations] = useScopeValue( 'pdf.logEntryAnnotations' ) // the URL for downloading the PDF const [pdfDownloadUrl, setPdfDownloadUrl] = useScopeValue('pdf.downloadUrl') // the URL for loading the PDF in the preview pane const [pdfUrl, setPdfUrl] = useScopeValue('pdf.url') // the project is considered to be "uncompiled" if a doc has changed since the last compile started const [uncompiled, setUncompiled] = useScopeValue('pdf.uncompiled') // the id of the CLSI server which ran the compile const [clsiServerId, setClsiServerId] = useScopeValue('pdf.clsiServerId') // data received in response to a compile request const [data, setData] = useState() // callback to be invoked for PdfJsMetrics const [firstRenderDone, setFirstRenderDone] = useState() // whether the project has been compiled yet const [compiledOnce, setCompiledOnce] = useState(false) // whether the cache is being cleared const [clearingCache, setClearingCache] = useState(false) // whether the logs should be visible const [showLogs, setShowLogs] = useState(false) // an error that occurred const [error, setError] = useState() // the list of files that can be downloaded const [fileList, setFileList] = useState() // the raw contents of the log file const [rawLog, setRawLog] = useState() // validation issues from CLSI const [validationIssues, setValidationIssues] = useState() // whether autocompile is switched on const [autoCompile, _setAutoCompile] = usePersistedState( `autocompile_enabled:${projectId}`, false, true ) // whether the compile should run in draft mode const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, true) // whether compiling should be prevented if there are linting errors const [stopOnValidationError, setStopOnValidationError] = usePersistedState( `stop_on_validation_error:${projectId}`, true, true ) // the Document currently open in the editor const [currentDoc] = useScopeValue('editor.sharejs_doc') // whether the editor linter found errors const [hasLintingError, setHasLintingError] = useScopeValue('hasLintingError') // whether syntax validation is enabled globally const [syntaxValidation] = useScopeValue('settings.syntaxValidation') // the timestamp that a doc was last changed or saved const [changedAt, setChangedAt] = useState(0) const { signal } = useAbortController() // the document compiler const [compiler] = useState(() => { return new DocumentCompiler({ project, setChangedAt, setCompiling, setData, setFirstRenderDone, setError, signal, }) }) // keep currentDoc in sync with the compiler useEffect(() => { compiler.currentDoc = currentDoc }, [compiler, currentDoc]) // keep draft setting in sync with the compiler useEffect(() => { compiler.draft = draft }, [compiler, draft]) // pass the "uncompiled" value up into the scope for use outside this context provider useEffect(() => { if (window.showNewPdfPreview) { setUncompiled(changedAt > 0) } }, [setUncompiled, changedAt]) // record changes to the autocompile setting const setAutoCompile = useCallback( value => { _setAutoCompile(value) sendMB('autocompile-setting-changed', { value }) }, [_setAutoCompile] ) // always compile the PDF once after opening the project, after the doc has loaded useEffect(() => { if (!compiledOnce && currentDoc) { setCompiledOnce(true) compiler.compile({ isAutoCompileOnLoad: true }) } }, [compiledOnce, currentDoc, compiler]) // handle the data returned from a compile request // note: this should _only_ run when `data` changes, // the other dependencies must all be static useEffect(() => { if (data) { if (data.clsiServerId) { setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController compiler.clsiServerId = data.clsiServerId } if (data.compileGroup) { compiler.compileGroup = data.compileGroup } if (data.outputFiles) { handleOutputFiles(projectId, data).then(result => { setLogEntryAnnotations( buildLogEntryAnnotations(result.logEntries.all, ide.fileTreeManager) ) setLogEntries(result.logEntries) setFileList(result.fileList) setPdfDownloadUrl(result.pdfDownloadUrl) setPdfUrl(result.pdfUrl) setRawLog(result.log) // sample compile stats for real users if (!window.user.alphaProgram && data.status === 'success') { sendMBSampled( 'compile-result', { errors: result.logEntries.errors.length, warnings: result.logEntries.warnings.length, typesetting: result.logEntries.typesetting.length, newPdfPreview: true, // TODO: is this useful? }, 0.01 ) } }) } switch (data.status) { case 'success': setError(undefined) setShowLogs(false) break case 'clsi-maintenance': case 'compile-in-progress': case 'exited': case 'failure': case 'project-too-large': case 'rate-limited': case 'terminated': case 'too-recently-compiled': setError(data.status) break case 'timedout': setError('timedout') if (!hasPremiumCompile && isProjectOwner) { send( 'subscription-funnel', 'editor-click-feature', 'compile-timeout' ) sendMB('paywall-prompt', { 'paywall-type': 'compile-timeout', }) } break case 'autocompile-backoff': if (!data.options.isAutoCompileOnLoad) { setError('autocompile-disabled') setAutoCompile(false) sendMB('autocompile-rate-limited', { hasPremiumCompile }) } break case 'unavailable': setError('clsi-unavailable') break case 'validation-problems': setError('validation-problems') setValidationIssues(data.validationProblems) break default: setError('error') break } } }, [ compiler, data, ide, hasPremiumCompile, isProjectOwner, projectId, setAutoCompile, setClsiServerId, setLogEntries, setLogEntryAnnotations, setPdfDownloadUrl, setPdfUrl, ]) // switch to logs if there's an error useEffect(() => { if (error) { setShowLogs(true) } }, [error]) // recompile on key press useEffect(() => { const listener = event => { compiler.compile(event.detail) } window.addEventListener('pdf:recompile', listener) return () => { window.removeEventListener('pdf:recompile', listener) } }, [compiler]) // whether there has been an autocompile linting error, if syntax validation is switched on const autoCompileLintingError = Boolean( autoCompile && syntaxValidation && hasLintingError ) const codeCheckFailed = stopOnValidationError && autoCompileLintingError // show that the project has pending changes const hasChanges = Boolean( autoCompile && uncompiled && compiledOnce && !codeCheckFailed ) // the project is available for auto-compiling const canAutoCompile = Boolean(autoCompile && !compiling && !codeCheckFailed) // call the debounced autocompile function if the project is available for auto-compiling and it has changed useEffect(() => { if (canAutoCompile && changedAt > 0) { compiler.debouncedAutoCompile() } else { compiler.debouncedAutoCompile.cancel() } }, [compiler, canAutoCompile, changedAt]) // cancel debounced recompile on unmount useEffect(() => { return () => { compiler.debouncedAutoCompile.cancel() } }, [compiler]) // record doc changes when notified by the editor useEffect(() => { const listener = event => { setChangedAt(Date.now()) } window.addEventListener('doc:changed', listener) window.addEventListener('doc:saved', listener) return () => { window.removeEventListener('doc:changed', listener) window.removeEventListener('doc:saved', listener) } }, []) // start a compile manually const startCompile = useCallback(() => { compiler.compile() }, [compiler]) // stop a compile manually const stopCompile = useCallback(() => { compiler.stopCompile() }, [compiler]) // clear the compile cache const clearCache = useCallback(() => { setClearingCache(true) return compiler.clearCache().finally(() => { setClearingCache(false) }) }, [compiler, setClearingCache]) // clear the cache then run a compile, triggered by a menu item const recompileFromScratch = useCallback(() => { clearCache().then(() => { compiler.compile() }) }, [clearCache, compiler]) const value = useMemo( () => ({ autoCompile, clearCache, clearingCache, clsiServerId, codeCheckFailed, compiling, draft, error, fileList, hasChanges, hasLintingError, logEntries, logEntryAnnotations, pdfDownloadUrl, pdfUrl, rawLog, recompileFromScratch, setAutoCompile, setClearingCache, setCompiling, setDraft, setError, setHasLintingError, // only for stories setShowLogs, setStopOnValidationError, showLogs, startCompile, stopCompile, stopOnValidationError, uncompiled, validationIssues, firstRenderDone, }), [ autoCompile, clearCache, clearingCache, clsiServerId, codeCheckFailed, compiling, draft, error, fileList, hasChanges, hasLintingError, logEntries, logEntryAnnotations, pdfDownloadUrl, pdfUrl, rawLog, recompileFromScratch, setAutoCompile, setDraft, setError, setHasLintingError, setStopOnValidationError, showLogs, startCompile, stopCompile, stopOnValidationError, uncompiled, validationIssues, firstRenderDone, ] ) return ( {children} ) } CompileProvider.propTypes = { children: PropTypes.any, } export function useCompileContext(propTypes) { const data = useContext(CompileContext) PropTypes.checkPropTypes(propTypes, data, 'data', 'CompileContext.Provider') return data }