first commit
This commit is contained in:
331
services/web/frontend/js/ide/connection/SocketIoShim.js
Normal file
331
services/web/frontend/js/ide/connection/SocketIoShim.js
Normal file
@@ -0,0 +1,331 @@
|
||||
/* global io */
|
||||
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import EventEmitter from '@/utils/EventEmitter'
|
||||
|
||||
class SocketShimBase {
|
||||
static connect(url, options) {
|
||||
return new SocketShimBase()
|
||||
}
|
||||
|
||||
constructor(socket) {
|
||||
this._socket = socket
|
||||
}
|
||||
|
||||
forceDisconnectWithoutEvent() {}
|
||||
}
|
||||
const transparentMethods = [
|
||||
'connect',
|
||||
'disconnect',
|
||||
'emit',
|
||||
'on',
|
||||
'removeListener',
|
||||
]
|
||||
for (const method of transparentMethods) {
|
||||
SocketShimBase.prototype[method] = function () {
|
||||
this._socket[method].apply(this._socket, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
class SocketShimNoop extends SocketShimBase {
|
||||
static connect() {
|
||||
return new SocketShimNoop()
|
||||
}
|
||||
|
||||
constructor(socket) {
|
||||
super(socket)
|
||||
this.socket = {
|
||||
get connected() {
|
||||
return false
|
||||
},
|
||||
get sessionid() {
|
||||
return undefined
|
||||
},
|
||||
get transport() {
|
||||
return {}
|
||||
},
|
||||
|
||||
connect() {},
|
||||
disconnect(reason) {},
|
||||
}
|
||||
}
|
||||
|
||||
connect() {}
|
||||
disconnect(reason) {}
|
||||
emit() {}
|
||||
on() {}
|
||||
removeListener() {}
|
||||
}
|
||||
|
||||
class SocketShimV0 extends SocketShimBase {
|
||||
static connect(url, options) {
|
||||
return new SocketShimV0(io.connect(url, options))
|
||||
}
|
||||
|
||||
constructor(socket) {
|
||||
super(socket)
|
||||
this.socket = this._socket.socket
|
||||
const self = this
|
||||
Object.defineProperty(this.socket, 'transport', {
|
||||
get() {
|
||||
return self._transport
|
||||
},
|
||||
set(v) {
|
||||
self.forceDisconnectWithoutEvent()
|
||||
self._transport = v
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
forceDisconnectWithoutEvent() {
|
||||
clearTimeout(this.socket.heartbeatTimeoutTimer)
|
||||
if (this._transport) this.forceCloseTransport(this._transport)
|
||||
}
|
||||
|
||||
forceCloseTransport(transport) {
|
||||
transport.clearTimeouts()
|
||||
if (transport instanceof io.Transport.websocket) {
|
||||
// retry closing
|
||||
transport.websocket.onopen = transport.websocket.onmessage = () =>
|
||||
transport.websocket.close()
|
||||
// mute close/error handler
|
||||
transport.websocket.onclose = transport.websocket.onerror = () => {}
|
||||
// disconnect
|
||||
try {
|
||||
transport.websocket.close()
|
||||
} catch {}
|
||||
} else if (transport instanceof io.Transport['xhr-polling']) {
|
||||
// mute data/close handler and block new polling GET requests
|
||||
transport.onData = transport.onClose = transport.get = () => {}
|
||||
// abort pending long-polling/POST request
|
||||
for (const xhr of [transport.xhr, transport.sendXHR]) {
|
||||
if (!xhr) continue // not pending
|
||||
// mute xhr callbacks
|
||||
xhr.onreadystatechange = xhr.onload = xhr.onerror = () => {}
|
||||
try {
|
||||
xhr.abort()
|
||||
} catch {}
|
||||
}
|
||||
transport.xhr = transport.sendXHR = null
|
||||
// Mark long-polling client as disconnected to avoid "ghost" connected client.
|
||||
fetch(transport.prepareUrl() + '/?disconnect=1', {
|
||||
// Let the request continue after navigating away from or reloading the page.
|
||||
keepalive: true,
|
||||
})
|
||||
// Avoid leaving a dangling response on the wire.
|
||||
.then(res => res.text())
|
||||
.catch(() => {})
|
||||
} else {
|
||||
try {
|
||||
transport.close()
|
||||
} catch {}
|
||||
debugConsole.warn('unexpected socket.io transport', transport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SocketShimV2 extends SocketShimBase {
|
||||
static connect(url, options) {
|
||||
options.forceNew = options['force new connection']
|
||||
// .resource has no leading slash, path wants to see one.
|
||||
options.path = '/' + options.resource
|
||||
options.reconnection = options.reconnect
|
||||
options.timeout = options['connect timeout']
|
||||
return new SocketShimV2(url, options)
|
||||
}
|
||||
|
||||
static get EVENT_MAP() {
|
||||
// Use the v2 event names transparently to the frontend.
|
||||
const connectionFailureEvents = [
|
||||
'connect_error',
|
||||
'connect_timeout',
|
||||
'error',
|
||||
]
|
||||
return new Map([
|
||||
['connect_failed', connectionFailureEvents],
|
||||
['error', connectionFailureEvents],
|
||||
])
|
||||
}
|
||||
|
||||
_on(event, handler) {
|
||||
// Keep track of our event listeners.
|
||||
// We move them to a new socket in ._replaceSocketWithNewInstance()
|
||||
if (!this._events.has(event)) {
|
||||
this._events.set(event, [handler])
|
||||
} else {
|
||||
this._events.get(event).push(handler)
|
||||
}
|
||||
this._socket.on(event, handler)
|
||||
}
|
||||
|
||||
on(event, handler) {
|
||||
if (SocketShimV2.EVENT_MAP.has(event)) {
|
||||
for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
|
||||
this._on(v2Event, handler)
|
||||
}
|
||||
} else {
|
||||
this._on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
_removeListener(event, handler) {
|
||||
// Keep track of our event listeners.
|
||||
// We move them to a new socket in ._replaceSocketWithNewInstance()
|
||||
if (this._events.has(event)) {
|
||||
const listeners = this._events.get(event)
|
||||
const pos = listeners.indexOf(handler)
|
||||
if (pos !== -1) {
|
||||
listeners.splice(pos, 1)
|
||||
}
|
||||
}
|
||||
this._socket.removeListener(event, handler)
|
||||
}
|
||||
|
||||
removeListener(event, handler) {
|
||||
if (SocketShimV2.EVENT_MAP.has(event)) {
|
||||
for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
|
||||
this._removeListener(v2Event, handler)
|
||||
}
|
||||
} else {
|
||||
this._removeListener(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
static createNewSocket(url, options) {
|
||||
// open a brand new connection for the default namespace '/'
|
||||
// The old socket can still leak 'disconnect' events from the teardown
|
||||
// of the old transport. The leaking 'disconnect' events interfere with
|
||||
// the _new_ connection and cancel the new connect attempt.
|
||||
// Also skip the caching in these locations:
|
||||
// - `io.connect()` caches `io.Manager`s in `io.managers`
|
||||
// - `io.Manager().socket()` caches `io.Socket`s in its `this.nsps`
|
||||
return io.Manager(url, options).socket('/', options)
|
||||
}
|
||||
|
||||
_replaceSocketWithNewInstance() {
|
||||
const oldSocket = this._socket
|
||||
const newSocket = SocketShimV2.createNewSocket(this._url, this._options)
|
||||
|
||||
// move our existing event handlers to the new socket
|
||||
this._events.forEach((listeners, event) => {
|
||||
for (const listener of listeners) {
|
||||
oldSocket.removeListener(event, listener)
|
||||
newSocket.on(event, listener)
|
||||
}
|
||||
})
|
||||
|
||||
if (oldSocket.connected) {
|
||||
// We overwrite the reference to oldSocket soon.
|
||||
// Make sure we are disconnected.
|
||||
oldSocket.disconnect()
|
||||
}
|
||||
this._socket = newSocket
|
||||
}
|
||||
|
||||
connect() {
|
||||
// have the same logic behind socket.connect and socket.socket.connect
|
||||
this._replaceSocketWithNewInstance()
|
||||
}
|
||||
|
||||
constructor(url, options) {
|
||||
super(SocketShimV2.createNewSocket(url, options))
|
||||
this._url = url
|
||||
this._options = options
|
||||
this._events = new Map()
|
||||
|
||||
const self = this
|
||||
function _getEngine() {
|
||||
return (self._socket.io && self._socket.io.engine) || {}
|
||||
}
|
||||
|
||||
this.socket = {
|
||||
get connected() {
|
||||
return self._socket.connected
|
||||
},
|
||||
get sessionid() {
|
||||
if (self._socket.id) {
|
||||
return self._socket.id
|
||||
}
|
||||
// socket.id is discarded upon disconnect
|
||||
// the id is still available in the internal state
|
||||
return _getEngine().id
|
||||
},
|
||||
get transport() {
|
||||
return _getEngine().transport
|
||||
},
|
||||
|
||||
connect() {
|
||||
self._replaceSocketWithNewInstance()
|
||||
},
|
||||
disconnect(reason) {
|
||||
return self._socket.disconnect(reason)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current
|
||||
if (typeof io === 'undefined' || !io) {
|
||||
debugConsole.log('[socket.io] Shim: socket.io is not loaded, returning noop')
|
||||
current = SocketShimNoop
|
||||
} else if (typeof io.version === 'string' && io.version.slice(0, 1) === '0') {
|
||||
debugConsole.log('[socket.io] Shim: detected v0')
|
||||
current = SocketShimV0
|
||||
} else {
|
||||
// socket.io v2 does not have a global io.version attribute.
|
||||
debugConsole.log('[socket.io] Shim: detected v2')
|
||||
current = SocketShimV2
|
||||
}
|
||||
|
||||
export class SocketIOMock extends SocketShimBase {
|
||||
constructor() {
|
||||
super(new EventEmitter())
|
||||
this.socket = {
|
||||
get connected() {
|
||||
return false
|
||||
},
|
||||
get sessionid() {
|
||||
return undefined
|
||||
},
|
||||
get transport() {
|
||||
return {}
|
||||
},
|
||||
get transports() {
|
||||
return []
|
||||
},
|
||||
|
||||
connect() {},
|
||||
disconnect(reason) {},
|
||||
}
|
||||
}
|
||||
|
||||
addListener(event, listener) {
|
||||
this._socket.on(event, listener)
|
||||
}
|
||||
|
||||
removeListener(event, listener) {
|
||||
this._socket.off(event, listener)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.emitToClient('disconnect')
|
||||
}
|
||||
|
||||
emitToClient(...args) {
|
||||
// Round-trip through JSON.parse/stringify to simulate (de-)serializing on network layer.
|
||||
this.emit(...JSON.parse(JSON.stringify(args)))
|
||||
}
|
||||
|
||||
countEventListeners(event) {
|
||||
return this._socket.events[event].length
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
SocketShimNoop,
|
||||
SocketShimV0,
|
||||
SocketShimV2,
|
||||
current,
|
||||
connect: current.connect,
|
||||
stub: () => new SocketShimNoop(),
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
import LatexLogParser from '../log-parser/latex-log-parser'
|
||||
import ruleset from './HumanReadableLogsRules'
|
||||
|
||||
export default {
|
||||
parse(rawLog, options) {
|
||||
const parsedLogEntries =
|
||||
typeof rawLog === 'string'
|
||||
? new LatexLogParser(rawLog, options).parse()
|
||||
: rawLog
|
||||
|
||||
const seenErrorTypes = {} // keep track of types of errors seen
|
||||
|
||||
for (const entry of parsedLogEntries.all) {
|
||||
const ruleDetails = ruleset.find(rule =>
|
||||
rule.regexToMatch.test(entry.message)
|
||||
)
|
||||
|
||||
if (ruleDetails) {
|
||||
if (ruleDetails.ruleId) {
|
||||
entry.ruleId = ruleDetails.ruleId
|
||||
}
|
||||
|
||||
if (ruleDetails.newMessage) {
|
||||
entry.message = entry.message.replace(
|
||||
ruleDetails.regexToMatch,
|
||||
ruleDetails.newMessage
|
||||
)
|
||||
}
|
||||
|
||||
if (ruleDetails.contentRegex) {
|
||||
const match = entry.content.match(ruleDetails.contentRegex)
|
||||
if (match) {
|
||||
entry.contentDetails = match.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.contentDetails && ruleDetails.improvedTitle) {
|
||||
const message = ruleDetails.improvedTitle(
|
||||
entry.message,
|
||||
entry.contentDetails
|
||||
)
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
entry.message = message[0]
|
||||
// removing the messageComponent, as the markup possible in it was causing crashes when
|
||||
// attempting to broadcast it in the detach-context (cant structuredClone an html node)
|
||||
// see https://github.com/overleaf/internal/discussions/15031 for context
|
||||
// entry.messageComponent = message[1]
|
||||
} else {
|
||||
entry.message = message
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.contentDetails && ruleDetails.highlightCommand) {
|
||||
entry.command = ruleDetails.highlightCommand(entry.contentDetails)
|
||||
}
|
||||
|
||||
// suppress any entries that are known to cascade from previous error types
|
||||
if (ruleDetails.cascadesFrom) {
|
||||
for (const type of ruleDetails.cascadesFrom) {
|
||||
if (seenErrorTypes[type]) {
|
||||
entry.suppressed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// record the types of errors seen
|
||||
if (ruleDetails.types) {
|
||||
for (const type of ruleDetails.types) {
|
||||
seenErrorTypes[type] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter out the suppressed errors (from the array entries in parsedLogEntries)
|
||||
for (const [key, errors] of Object.entries(parsedLogEntries)) {
|
||||
if (typeof errors === 'object' && errors.length > 0) {
|
||||
parsedLogEntries[key] = Array.from(errors).filter(
|
||||
err => !err.suppressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedLogEntries
|
||||
},
|
||||
}
|
@@ -0,0 +1,524 @@
|
||||
import {
|
||||
packageSuggestionsForCommands,
|
||||
packageSuggestionsForEnvironments,
|
||||
} from './HumanReadableLogsPackageSuggestions'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function WikiLink({
|
||||
url,
|
||||
children,
|
||||
}: {
|
||||
url: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
if (getMeta('ol-wikiEnabled')) {
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return <>{children}</>
|
||||
}
|
||||
}
|
||||
|
||||
type LogHint = {
|
||||
extraInfoURL?: string | null
|
||||
formattedContent: (details?: string[]) => React.ReactNode
|
||||
}
|
||||
|
||||
const hints: { [ruleId: string]: LogHint } = {
|
||||
hint_misplaced_alignment_tab_character: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Misplaced_alignment_tab_character_%26',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have placed an alignment tab character '&' in the wrong place. If
|
||||
you want to align something, you must write it inside an align
|
||||
environment such as \begin
|
||||
{'{align}'} … \end
|
||||
{'{align}'}, \begin
|
||||
{'{tabular}'} … \end
|
||||
{'{tabular}'}, etc. If you want to write an ampersand '&' in text, you
|
||||
must write \& instead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_extra_alignment_tab_has_been_changed: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have written too many alignment tabs in a table, causing one of them
|
||||
to be turned into a line break. Make sure you have specified the correct
|
||||
number of columns in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Tables">table</WikiLink>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_display_math_should_end_with: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Display_math_should_end_with_$$',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have forgotten a $ sign at the end of 'display math' mode. When
|
||||
writing in display math mode, you must always math write inside $$ … $$.
|
||||
Check that the number of $s match around each math expression.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_missing_inserted: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Missing_$_inserted',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
<p>
|
||||
You need to enclose all mathematical expressions and symbols with
|
||||
special markers. These special markers create a ‘math mode’.
|
||||
</p>
|
||||
<p>
|
||||
Use <code>$...$</code> for inline math mode, and <code>\[...\]</code>
|
||||
or one of the mathematical environments (e.g. equation) for display
|
||||
math mode.
|
||||
</p>
|
||||
<p>
|
||||
This applies to symbols such as subscripts ( <code>_</code> ),
|
||||
integrals ( <code>\int</code> ), Greek letters ( <code>\alpha</code>,{' '}
|
||||
<code>\beta</code>, <code>\delta</code> ) and modifiers{' '}
|
||||
<code>{'(\\vec{x}'}</code>, <code>{'\\tilde{x}'})</code>.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_reference_undefined: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_undefined_references',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have referenced something which has not yet been labelled. If you
|
||||
have labelled it already, make sure that what is written inside \ref
|
||||
{'{...}'} is the same as what is written inside \label
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_there_were_undefined_references: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_undefined_references',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have referenced something which has not yet been labelled. If you
|
||||
have labelled it already, make sure that what is written inside \ref
|
||||
{'{...}'} is the same as what is written inside \label
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_citation_on_page_undefined_on_input_line: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have cited something which is not included in your bibliography.
|
||||
Make sure that the citation (\cite
|
||||
{'{...}'}) has a corresponding key in your bibliography, and that both
|
||||
are spelled the same way.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_label_multiply_defined_labels: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/There_were_multiply-defined_labels',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used the same label more than once. Check that each \label
|
||||
{'{...}'} labels only one item.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_float_specifier_changed: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
The float specifier 'h' is too strict of a demand for LaTeX to place
|
||||
your float in a nice way here. Try relaxing it by using 'ht', or even
|
||||
'htbp' if necessary. If you want to try keep the float here anyway,
|
||||
check out the{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
float package
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_no_positions_in_optional_float_specifier: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/No_positions_in_optional_float_specifier',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have forgotten to include a float specifier, which tells LaTeX where
|
||||
to position your figure. To fix this, either insert a float specifier
|
||||
inside the square brackets (e.g. \begin
|
||||
{'{figure}'}
|
||||
[h]), or remove the square brackets (e.g. \begin
|
||||
{'{figure}'}
|
||||
). Find out more about float specifiers{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
here
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_undefined_control_sequence: {
|
||||
formattedContent: details => {
|
||||
if (details?.length && packageSuggestionsForCommands.has(details[0])) {
|
||||
const command = details[0]
|
||||
const suggestion = packageSuggestionsForCommands.get(command)
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
We think you’ve got a missing package! The <code>{command}</code>{' '}
|
||||
command won't work unless you include
|
||||
<code>{suggestion.command}</code> in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
|
||||
document preamble
|
||||
</WikiLink>
|
||||
.{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
|
||||
Learn more about packages
|
||||
</WikiLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
The compiler is having trouble understanding a command you have used.
|
||||
Check that the command is spelled correctly. If the command is part of
|
||||
a package, make sure you have included the package in your preamble
|
||||
using <code>\usepackage</code>
|
||||
{'{...}'}.
|
||||
<div className="log-entry-content-link">
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/Errors/Undefined_control_sequence"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
hint_undefined_environment: {
|
||||
formattedContent: details => {
|
||||
if (
|
||||
details?.length &&
|
||||
packageSuggestionsForEnvironments.has(details[0])
|
||||
) {
|
||||
const environment = details[0]
|
||||
const suggestion = packageSuggestionsForEnvironments.get(environment)
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
We think you’ve got a missing package! The{' '}
|
||||
<code>{environment}</code> environment won't work unless you
|
||||
include <code>{suggestion.command}</code> in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
|
||||
document preamble
|
||||
</WikiLink>
|
||||
.{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
|
||||
Learn more about packages
|
||||
</WikiLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
You have created an environment (using \begin
|
||||
{'{…}'} and \end
|
||||
{'{…}'} commands) which is not recognized. Make sure you have included
|
||||
the required package for that environment in your preamble, and that
|
||||
the environment is spelled correctly.
|
||||
<div className="log-entry-content-link">
|
||||
<a
|
||||
href="https://www.overleaf.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
hint_file_not_found: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
The compiler cannot find the file you want to include. Make sure that
|
||||
you have{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/how-to/Including_images_on_Overleaf">
|
||||
uploaded the file
|
||||
</WikiLink>{' '}
|
||||
and{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/File_XXX_not_found_on_input_line_XXX.">
|
||||
specified the file location correctly
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_unknown_graphics_extension: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
The compiler does not recognise the file type of one of your images.
|
||||
Make sure you are using a{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif.">
|
||||
supported image format
|
||||
</WikiLink>{' '}
|
||||
for your choice of compiler, and check that there are no periods (.) in
|
||||
the name of your image.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_unknown_float_option_h: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
The compiler isn't recognizing the float option 'H'. Include \usepackage
|
||||
{'{float}'} in your preamble to fix this.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_unknown_float_option_q: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used a float specifier which the compiler does not understand.
|
||||
You can learn more about the different float options available for
|
||||
placing figures{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Positioning_of_Figures">
|
||||
here
|
||||
</WikiLink>{' '}
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_math_allowed_only_in_math_mode: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used a font command which is only available in math mode. To
|
||||
use this command, you must be in maths mode (E.g. $ … $ or \begin
|
||||
{'{math}'} … \end
|
||||
{'{math}'}
|
||||
). If you want to use it outside of math mode, use the text version
|
||||
instead: \textrm, \textit, etc.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment: {
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{...}'} without a corresponding \end
|
||||
{'{...}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_brackets: {
|
||||
formattedContent: () => (
|
||||
<>You have used an open bracket without a corresponding close bracket.</>
|
||||
),
|
||||
},
|
||||
hint_can_be_used_only_in_preamble: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used a command in the main body of your document which should
|
||||
be used in the preamble. Make sure that \documentclass[…]
|
||||
{'{…}'} and all \usepackage
|
||||
{'{…}'} commands are written before \begin
|
||||
{'{document}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_missing_right_inserted: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/Missing_%5Cright_insertede',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have started an expression with a \left command, but have not
|
||||
included a corresponding \right command. Make sure that your \left and
|
||||
\right commands balance everywhere, or else try using \Biggl and \Biggr
|
||||
commands instead as shown{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/Errors/Missing_%5Cright_inserted">
|
||||
here
|
||||
</WikiLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_double_superscript: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Double_superscript',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have written a double superscript incorrectly as a^b^c, or else you
|
||||
have written a prime with a superscript. Remember to include {'{and}'}{' '}
|
||||
when using multiple superscripts. Try a^
|
||||
{'{b ^ c}'} instead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_double_subscript: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Double_subscript',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have written a double subscript incorrectly as a_b_c. Remember to
|
||||
include {'{and}'} when using multiple subscripts. Try a_
|
||||
{'{b_c}'} instead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_no_author_given: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/No_%5Cauthor_given',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used the \maketitle command, but have not specified any
|
||||
\author. To fix this, include an author in your preamble using the
|
||||
\author
|
||||
{'{…}'} command.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_somethings_wrong_perhaps_a_missing_item: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
There are no entries found in a list you have created. Make sure you
|
||||
label list entries using the \item command, and that you have not used a
|
||||
list inside a table.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_misplaced_noalign: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Misplaced_%5Cnoalign',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used a \hline command in the wrong place, probably outside a
|
||||
table. If the \hline command is written inside a table, try including \\
|
||||
before it.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_no_line_here_to_end: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_There%27s_no_line_here_to_end',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used a \\ or \newline command where LaTeX was not expecting
|
||||
one. Make sure that you only use line breaks after blocks of text, and
|
||||
be careful using linebreaks inside lists and other environments.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_verb_ended_by_end_of_line: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors/LaTeX_Error:_%5Cverb_ended_by_end_of_line',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used a \verb command incorrectly. Try replacling the \verb
|
||||
command with \begin
|
||||
{'{verbatim}'}
|
||||
…\end
|
||||
{'{verbatim}'}
|
||||
.\
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_illegal_unit_of_measure_pt_inserted: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FIllegal%20unit%20of%20measure%20(pt%20inserted)',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have written a length, but have not specified the appropriate units
|
||||
(pt, mm, cm etc.). If you have not written a length, check that you have
|
||||
not witten a linebreak \\ followed by square brackets […] anywhere.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_extra_right: {
|
||||
extraInfoURL: 'https://www.overleaf.com/learn/Errors/Extra_%5Cright',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have written a \right command without a corresponding \left command.
|
||||
Check that all \left and \right commands balance everywhere.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_missing_begin_document_: {
|
||||
extraInfoURL:
|
||||
'https://www.overleaf.com/learn/Errors%2FLaTeX%20Error%3A%20Missing%20%5Cbegin%20document',
|
||||
formattedContent: () => (
|
||||
<>
|
||||
No \begin
|
||||
{'{document}'} command was found. Make sure you have included \begin
|
||||
{'{document}'} in your preamble, and that your main document is set
|
||||
correctly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment2: {
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment3: {
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
hint_mismatched_environment4: {
|
||||
formattedContent: () => (
|
||||
<>
|
||||
You have used \begin
|
||||
{'{}'} without a corresponding \end
|
||||
{'{}'}.
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const ruleIds = Object.keys(hints)
|
||||
|
||||
if (!getMeta('ol-wikiEnabled')) {
|
||||
Object.keys(hints).forEach(ruleId => {
|
||||
hints[ruleId].extraInfoURL = null
|
||||
})
|
||||
}
|
||||
|
||||
export default hints
|
@@ -0,0 +1,49 @@
|
||||
const commandSuggestions = [
|
||||
[
|
||||
'\\includegraphics',
|
||||
{ name: 'graphicx', command: '\\usepackage{graphicx}' },
|
||||
],
|
||||
['\\toprule', { name: 'booktabs', command: '\\usepackage{booktabs}' }],
|
||||
['\\midrule', { name: 'booktabs', command: '\\usepackage{booktabs}' }],
|
||||
['\\bottomrule', { name: 'booktabs', command: '\\usepackage{booktabs}' }],
|
||||
['\\cmidrule', { name: 'booktabs', command: '\\usepackage{booktabs}' }],
|
||||
['\\multirow', { name: 'multirow', command: '\\usepackage{multirow}' }],
|
||||
['\\justifying', { name: 'ragged2e', command: '\\usepackage{ragged2e}' }],
|
||||
['\\tag', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\notag', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\text', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\boldsymbol', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\eqref', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\iint', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\iiint', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['\\nmid', { name: 'amssymb', command: '\\usepackage{amssymb}' }],
|
||||
['\\varnothing', { name: 'amssymb', command: '\\usepackage{amssymb}' }],
|
||||
['\\Box', { name: 'amssymb', command: '\\usepackage{amssymb}' }],
|
||||
['\\citep', { name: 'natbib', command: '\\usepackage{natbib}' }],
|
||||
['\\citet', { name: 'natbib', command: '\\usepackage{natbib}' }],
|
||||
['\\citepalias', { name: 'natbib', command: '\\usepackage{natbib}' }],
|
||||
['\\citetalias', { name: 'natbib', command: '\\usepackage{natbib}' }],
|
||||
['\\url', { name: 'url', command: '\\usepackage{url}' }],
|
||||
['\\href', { name: 'hyperref', command: '\\usepackage{hyperref}' }],
|
||||
['\\texorpdfstring', { name: 'hyperref', command: '\\usepackage{hyperref}' }],
|
||||
['\\phantomsection', { name: 'hyperref', command: '\\usepackage{hyperref}' }],
|
||||
['\\arraybackslash', { name: 'array', command: '\\usepackage{array}' }],
|
||||
]
|
||||
|
||||
const environmentSuggestions = [
|
||||
['justify', { name: 'ragged2e', command: '\\usepackage{ragged2e}' }],
|
||||
['align', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['align*', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['split', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['gather', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['cases', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['matrix', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['pmatrix', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['bmatrix', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['subequations', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
]
|
||||
|
||||
const packageSuggestionsForCommands = new Map(commandSuggestions)
|
||||
const packageSuggestionsForEnvironments = new Map(environmentSuggestions)
|
||||
|
||||
export { packageSuggestionsForCommands, packageSuggestionsForEnvironments }
|
File diff suppressed because it is too large
Load Diff
229
services/web/frontend/js/ide/log-parser/bib-log-parser.js
Normal file
229
services/web/frontend/js/ide/log-parser/bib-log-parser.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// [fullLine, lineNumber, messageType, message]
|
||||
const LINE_SPLITTER_REGEX = /^\[(\d+)].*>\s(INFO|WARN|ERROR)\s-\s(.*)$/
|
||||
|
||||
const MULTILINE_WARNING_REGEX = /^Warning--(.+)\n--line (\d+) of file (.+)$/m
|
||||
const SINGLELINE_WARNING_REGEX = /^Warning--(.+)$/m
|
||||
const MULTILINE_ERROR_REGEX =
|
||||
/^(.*)---line (\d+) of file (.*)\n([^]+?)\nI'm skipping whatever remains of this entry$/m
|
||||
const BAD_CROSS_REFERENCE_REGEX =
|
||||
/^(A bad cross reference---entry ".+?"\nrefers to entry.+?, which doesn't exist)$/m
|
||||
const MULTILINE_COMMAND_ERROR_REGEX =
|
||||
/^(.*)\n?---line (\d+) of file (.*)\n([^]+?)\nI'm skipping whatever remains of this command$/m
|
||||
// Errors hit in BST file have a slightly different format
|
||||
const BST_ERROR_REGEX = /^(.*?)\nwhile executing---line (\d+) of file (.*)/m
|
||||
|
||||
const MESSAGE_LEVELS = {
|
||||
INFO: 'info',
|
||||
WARN: 'warning',
|
||||
ERROR: 'error',
|
||||
}
|
||||
|
||||
const parserReducer = function (maxErrors) {
|
||||
return function (accumulator, parser) {
|
||||
const consume = function (logText, regex, process) {
|
||||
let match
|
||||
let text = logText
|
||||
const result = []
|
||||
let iterationCount = 0
|
||||
|
||||
while ((match = regex.exec(text))) {
|
||||
iterationCount++
|
||||
const newEntry = process(match)
|
||||
|
||||
// Too many log entries can cause browser crashes
|
||||
// Construct a too many files error from the last match
|
||||
if (maxErrors != null && iterationCount >= maxErrors) {
|
||||
return [result, '']
|
||||
}
|
||||
|
||||
result.push(newEntry)
|
||||
text =
|
||||
match.input.slice(0, match.index) +
|
||||
match.input.slice(
|
||||
match.index + match[0].length + 1,
|
||||
match.input.length
|
||||
)
|
||||
}
|
||||
|
||||
return [result, text]
|
||||
}
|
||||
|
||||
const [currentErrors, text] = accumulator
|
||||
const [regex, process] = parser
|
||||
const [errors, _remainingText] = consume(text, regex, process)
|
||||
return [currentErrors.concat(errors), _remainingText]
|
||||
}
|
||||
}
|
||||
|
||||
export default class BibLogParser {
|
||||
constructor(text, options = {}) {
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error('BibLogParser Error: text parameter must be a string')
|
||||
}
|
||||
this.text = text.replace(/(\r\n)|\r/g, '\n')
|
||||
this.options = options
|
||||
this.lines = text.split('\n')
|
||||
|
||||
// each parser is a pair of [regex, processFunction], where processFunction
|
||||
// describes how to transform the regex mactch into a log entry object.
|
||||
this.warningParsers = [
|
||||
[
|
||||
MULTILINE_WARNING_REGEX,
|
||||
function (match) {
|
||||
const [fullMatch, message, lineNumber, fileName] = match
|
||||
return {
|
||||
file: fileName,
|
||||
level: 'warning',
|
||||
message,
|
||||
line: lineNumber,
|
||||
raw: fullMatch,
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
SINGLELINE_WARNING_REGEX,
|
||||
function (match) {
|
||||
const [fullMatch, message] = match
|
||||
return {
|
||||
file: '',
|
||||
level: 'warning',
|
||||
message,
|
||||
line: '',
|
||||
raw: fullMatch,
|
||||
}
|
||||
},
|
||||
],
|
||||
]
|
||||
this.errorParsers = [
|
||||
[
|
||||
MULTILINE_ERROR_REGEX,
|
||||
function (match) {
|
||||
const [fullMatch, firstMessage, lineNumber, fileName, secondMessage] =
|
||||
match
|
||||
return {
|
||||
file: fileName,
|
||||
level: 'error',
|
||||
message: firstMessage + '\n' + secondMessage,
|
||||
line: lineNumber,
|
||||
raw: fullMatch,
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
BAD_CROSS_REFERENCE_REGEX,
|
||||
function (match) {
|
||||
const [fullMatch, message] = match
|
||||
return {
|
||||
file: '',
|
||||
level: 'error',
|
||||
message,
|
||||
line: '',
|
||||
raw: fullMatch,
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
MULTILINE_COMMAND_ERROR_REGEX,
|
||||
function (match) {
|
||||
const [fullMatch, firstMessage, lineNumber, fileName, secondMessage] =
|
||||
match
|
||||
return {
|
||||
file: fileName,
|
||||
level: 'error',
|
||||
message: firstMessage + '\n' + secondMessage,
|
||||
line: lineNumber,
|
||||
raw: fullMatch,
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
BST_ERROR_REGEX,
|
||||
function (match) {
|
||||
const [fullMatch, firstMessage, lineNumber, fileName] = match
|
||||
return {
|
||||
file: fileName,
|
||||
level: 'error',
|
||||
message: firstMessage,
|
||||
line: lineNumber,
|
||||
raw: fullMatch,
|
||||
}
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
parseBibtex() {
|
||||
// reduce over the parsers, starting with the log text,
|
||||
const [allWarnings, remainingText] = this.warningParsers.reduce(
|
||||
parserReducer(this.options.maxErrors),
|
||||
[[], this.text]
|
||||
)
|
||||
const [allErrors] = this.errorParsers.reduce(
|
||||
parserReducer(this.options.maxErrors),
|
||||
[[], remainingText]
|
||||
)
|
||||
|
||||
return {
|
||||
all: allWarnings.concat(allErrors),
|
||||
errors: allErrors,
|
||||
warnings: allWarnings,
|
||||
files: [], // not used
|
||||
typesetting: [], // not used
|
||||
}
|
||||
}
|
||||
|
||||
parseBiber() {
|
||||
const result = {
|
||||
all: [],
|
||||
errors: [],
|
||||
warnings: [],
|
||||
files: [], // not used
|
||||
typesetting: [], // not used
|
||||
}
|
||||
this.lines.forEach(function (line) {
|
||||
const match = line.match(LINE_SPLITTER_REGEX)
|
||||
if (match) {
|
||||
const [fullLine, , messageType, message] = match
|
||||
const newEntry = {
|
||||
file: '',
|
||||
level: MESSAGE_LEVELS[messageType] || 'INFO',
|
||||
message,
|
||||
line: '',
|
||||
raw: fullLine,
|
||||
}
|
||||
// try extract file, line-number and the 'real' message from lines like:
|
||||
// BibTeX subsystem: /.../original.bib_123.utf8, line 8, syntax error: it's bad
|
||||
const lineMatch = newEntry.message.match(
|
||||
/^BibTeX subsystem: \/.+\/(\w+\.\w+)_.+, line (\d+), (.+)$/
|
||||
)
|
||||
if (lineMatch) {
|
||||
const [, fileName, lineNumber, realMessage] = lineMatch
|
||||
newEntry.file = fileName
|
||||
newEntry.line = lineNumber
|
||||
newEntry.message = realMessage
|
||||
}
|
||||
result.all.push(newEntry)
|
||||
switch (newEntry.level) {
|
||||
case 'error':
|
||||
return result.errors.push(newEntry)
|
||||
case 'warning':
|
||||
return result.warnings.push(newEntry)
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
parse() {
|
||||
const firstLine = this.lines[0]
|
||||
if (firstLine.match(/^.*INFO - This is Biber.*$/)) {
|
||||
return this.parseBiber()
|
||||
} else if (firstLine.match(/^This is BibTeX, Version.+$/)) {
|
||||
return this.parseBibtex()
|
||||
} else {
|
||||
throw new Error(
|
||||
'BibLogParser Error: cannot determine whether text is biber or bibtex output'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
399
services/web/frontend/js/ide/log-parser/latex-log-parser.js
Normal file
399
services/web/frontend/js/ide/log-parser/latex-log-parser.js
Normal file
@@ -0,0 +1,399 @@
|
||||
// Define some constants
|
||||
const LOG_WRAP_LIMIT = 79
|
||||
const LATEX_WARNING_REGEX = /^LaTeX(?:3| Font)? Warning: (.*)$/
|
||||
const HBOX_WARNING_REGEX = /^(Over|Under)full \\(v|h)box/
|
||||
const PACKAGE_WARNING_REGEX = /^((?:Package|Class|Module) \b.+\b Warning:.*)$/
|
||||
// This is used to parse the line number from common latex warnings
|
||||
const LINES_REGEX = /lines? ([0-9]+)/
|
||||
// This is used to parse the package name from the package warnings
|
||||
const PACKAGE_REGEX = /^(?:Package|Class|Module) (\b.+\b) Warning/
|
||||
const FILE_LINE_ERROR_REGEX = /^([./].*):(\d+): (.*)/
|
||||
|
||||
const STATE = {
|
||||
NORMAL: 0,
|
||||
ERROR: 1,
|
||||
}
|
||||
|
||||
export default class LatexParser {
|
||||
constructor(text, options = {}) {
|
||||
this.state = STATE.NORMAL
|
||||
this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/]
|
||||
this.ignoreDuplicates = options.ignoreDuplicates
|
||||
this.data = []
|
||||
this.fileStack = []
|
||||
this.currentFileList = this.rootFileList = []
|
||||
this.openParens = 0
|
||||
this.latexWarningRegex = LATEX_WARNING_REGEX
|
||||
this.packageWarningRegex = PACKAGE_WARNING_REGEX
|
||||
this.packageRegex = PACKAGE_REGEX
|
||||
this.log = new LogText(text)
|
||||
}
|
||||
|
||||
parse() {
|
||||
while ((this.currentLine = this.log.nextLine()) !== false) {
|
||||
if (this.state === STATE.NORMAL) {
|
||||
if (this.currentLineIsError()) {
|
||||
this.state = STATE.ERROR
|
||||
this.currentError = {
|
||||
line: null,
|
||||
file: this.currentFilePath,
|
||||
level: 'error',
|
||||
message: this.currentLine.slice(2),
|
||||
content: '',
|
||||
raw: this.currentLine + '\n',
|
||||
}
|
||||
} else if (this.currentLineIsFileLineError()) {
|
||||
this.state = STATE.ERROR
|
||||
this.parseFileLineError()
|
||||
} else if (this.currentLineIsRunawayArgument()) {
|
||||
this.parseRunawayArgumentError()
|
||||
} else if (this.currentLineIsWarning()) {
|
||||
this.parseSingleWarningLine(this.latexWarningRegex)
|
||||
} else if (this.currentLineIsHboxWarning()) {
|
||||
this.parseHboxLine()
|
||||
} else if (this.currentLineIsPackageWarning()) {
|
||||
this.parseMultipleWarningLine()
|
||||
} else {
|
||||
this.parseParensForFilenames()
|
||||
}
|
||||
}
|
||||
if (this.state === STATE.ERROR) {
|
||||
this.currentError.content += this.log
|
||||
.linesUpToNextMatchingLine(/^l\.[0-9]+/)
|
||||
.join('\n')
|
||||
this.currentError.content += '\n'
|
||||
this.currentError.content += this.log
|
||||
.linesUpToNextWhitespaceLine(true)
|
||||
.join('\n')
|
||||
this.currentError.content += '\n'
|
||||
this.currentError.content += this.log
|
||||
.linesUpToNextWhitespaceLine(true)
|
||||
.join('\n')
|
||||
this.currentError.raw += this.currentError.content
|
||||
const lineNo = this.currentError.raw.match(/l\.([0-9]+)/)
|
||||
if (lineNo && this.currentError.line === null) {
|
||||
this.currentError.line = parseInt(lineNo[1], 10)
|
||||
}
|
||||
this.data.push(this.currentError)
|
||||
this.state = STATE.NORMAL
|
||||
}
|
||||
}
|
||||
return this.postProcess(this.data)
|
||||
}
|
||||
|
||||
currentLineIsError() {
|
||||
return (
|
||||
this.currentLine[0] === '!' &&
|
||||
this.currentLine !==
|
||||
'! ==> Fatal error occurred, no output PDF file produced!'
|
||||
)
|
||||
}
|
||||
|
||||
currentLineIsFileLineError() {
|
||||
return FILE_LINE_ERROR_REGEX.test(this.currentLine)
|
||||
}
|
||||
|
||||
currentLineIsRunawayArgument() {
|
||||
return this.currentLine.match(/^Runaway argument/)
|
||||
}
|
||||
|
||||
currentLineIsWarning() {
|
||||
return !!this.currentLine.match(this.latexWarningRegex)
|
||||
}
|
||||
|
||||
currentLineIsPackageWarning() {
|
||||
return !!this.currentLine.match(this.packageWarningRegex)
|
||||
}
|
||||
|
||||
currentLineIsHboxWarning() {
|
||||
return !!this.currentLine.match(HBOX_WARNING_REGEX)
|
||||
}
|
||||
|
||||
parseFileLineError() {
|
||||
const result = this.currentLine.match(FILE_LINE_ERROR_REGEX)
|
||||
this.currentError = {
|
||||
line: result[2],
|
||||
file: result[1],
|
||||
level: 'error',
|
||||
message: result[3],
|
||||
content: '',
|
||||
raw: this.currentLine + '\n',
|
||||
}
|
||||
}
|
||||
|
||||
parseRunawayArgumentError() {
|
||||
this.currentError = {
|
||||
line: null,
|
||||
file: this.currentFilePath,
|
||||
level: 'error',
|
||||
message: this.currentLine,
|
||||
content: '',
|
||||
raw: this.currentLine + '\n',
|
||||
}
|
||||
this.currentError.content += this.log
|
||||
.linesUpToNextWhitespaceLine()
|
||||
.join('\n')
|
||||
this.currentError.content += '\n'
|
||||
this.currentError.content += this.log
|
||||
.linesUpToNextWhitespaceLine()
|
||||
.join('\n')
|
||||
this.currentError.raw += this.currentError.content
|
||||
const lineNo = this.currentError.raw.match(/l\.([0-9]+)/)
|
||||
if (lineNo) {
|
||||
this.currentError.line = parseInt(lineNo[1], 10)
|
||||
}
|
||||
return this.data.push(this.currentError)
|
||||
}
|
||||
|
||||
parseSingleWarningLine(prefixRegex) {
|
||||
const warningMatch = this.currentLine.match(prefixRegex)
|
||||
if (!warningMatch) {
|
||||
return
|
||||
}
|
||||
const warning = warningMatch[1]
|
||||
const lineMatch = warning.match(LINES_REGEX)
|
||||
const line = lineMatch ? parseInt(lineMatch[1], 10) : null
|
||||
this.data.push({
|
||||
line,
|
||||
file: this.currentFilePath,
|
||||
level: 'warning',
|
||||
message: warning,
|
||||
raw: warning,
|
||||
})
|
||||
}
|
||||
|
||||
parseMultipleWarningLine() {
|
||||
// Some package warnings are multiple lines, let's parse the first line
|
||||
let warningMatch = this.currentLine.match(this.packageWarningRegex)
|
||||
// Something strange happened, return early
|
||||
if (!warningMatch) {
|
||||
return
|
||||
}
|
||||
const warningLines = [warningMatch[1]]
|
||||
let lineMatch = this.currentLine.match(LINES_REGEX)
|
||||
let line = lineMatch ? parseInt(lineMatch[1], 10) : null
|
||||
const packageMatch = this.currentLine.match(this.packageRegex)
|
||||
const packageName = packageMatch[1]
|
||||
// Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
|
||||
const prefixRegex = new RegExp(
|
||||
'(?:\\(' + packageName + '\\))*[\\s]*(.*)',
|
||||
'i'
|
||||
)
|
||||
// After every warning message there's a blank line, let's use it
|
||||
while ((this.currentLine = this.log.nextLine())) {
|
||||
lineMatch = this.currentLine.match(LINES_REGEX)
|
||||
line = lineMatch ? parseInt(lineMatch[1], 10) : line
|
||||
warningMatch = this.currentLine.match(prefixRegex)
|
||||
warningLines.push(warningMatch[1])
|
||||
}
|
||||
const rawMessage = warningLines.join(' ')
|
||||
this.data.push({
|
||||
line,
|
||||
file: this.currentFilePath,
|
||||
level: 'warning',
|
||||
message: rawMessage,
|
||||
raw: rawMessage,
|
||||
})
|
||||
}
|
||||
|
||||
parseHboxLine() {
|
||||
const lineMatch = this.currentLine.match(LINES_REGEX)
|
||||
const line = lineMatch ? parseInt(lineMatch[1], 10) : null
|
||||
this.data.push({
|
||||
line,
|
||||
file: this.currentFilePath,
|
||||
level: 'typesetting',
|
||||
message: this.currentLine,
|
||||
raw: this.currentLine,
|
||||
})
|
||||
}
|
||||
|
||||
// Check if we're entering or leaving a new file in this line
|
||||
|
||||
parseParensForFilenames() {
|
||||
const pos = this.currentLine.search(/[()]/)
|
||||
if (pos !== -1) {
|
||||
const token = this.currentLine[pos]
|
||||
this.currentLine = this.currentLine.slice(pos + 1)
|
||||
if (token === '(') {
|
||||
const filePath = this.consumeFilePath()
|
||||
if (filePath) {
|
||||
this.currentFilePath = filePath
|
||||
const newFile = {
|
||||
path: filePath,
|
||||
files: [],
|
||||
}
|
||||
this.fileStack.push(newFile)
|
||||
this.currentFileList.push(newFile)
|
||||
this.currentFileList = newFile.files
|
||||
} else {
|
||||
this.openParens++
|
||||
}
|
||||
} else if (token === ')') {
|
||||
if (this.openParens > 0) {
|
||||
this.openParens--
|
||||
} else {
|
||||
if (this.fileStack.length > 1) {
|
||||
this.fileStack.pop()
|
||||
const previousFile = this.fileStack[this.fileStack.length - 1]
|
||||
this.currentFilePath = previousFile.path
|
||||
this.currentFileList = previousFile.files
|
||||
}
|
||||
}
|
||||
}
|
||||
// else {
|
||||
// Something has gone wrong but all we can do now is ignore it :(
|
||||
// }
|
||||
// Process the rest of the line
|
||||
this.parseParensForFilenames()
|
||||
}
|
||||
}
|
||||
|
||||
consumeFilePath() {
|
||||
// Our heuristic for detecting file names are rather crude
|
||||
|
||||
// To contain a file path this line must have at least one / before any '(', ')' or '\'
|
||||
if (!this.currentLine.match(/^\/?([^ ()\\]+\/)+/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// A file may not contain a '(', ')' or '\'
|
||||
let endOfFilePath = this.currentLine.search(/[ ()\\]/)
|
||||
|
||||
// handle the case where there is a space in a filename
|
||||
while (endOfFilePath !== -1 && this.currentLine[endOfFilePath] === ' ') {
|
||||
const partialPath = this.currentLine.slice(0, endOfFilePath)
|
||||
// consider the file matching done if the space is preceded by a file extension (e.g. ".tex")
|
||||
if (/\.\w+$/.test(partialPath)) {
|
||||
break
|
||||
}
|
||||
// advance to next space or ) or end of line
|
||||
const remainingPath = this.currentLine.slice(endOfFilePath + 1)
|
||||
// consider file matching done if current path is followed by any of "()[]
|
||||
if (/^\s*["()[\]]/.test(remainingPath)) {
|
||||
break
|
||||
}
|
||||
const nextEndOfPath = remainingPath.search(/[ "()[\]]/)
|
||||
if (nextEndOfPath === -1) {
|
||||
endOfFilePath = -1
|
||||
} else {
|
||||
endOfFilePath += nextEndOfPath + 1
|
||||
}
|
||||
}
|
||||
let path
|
||||
if (endOfFilePath === -1) {
|
||||
path = this.currentLine
|
||||
this.currentLine = ''
|
||||
} else {
|
||||
path = this.currentLine.slice(0, endOfFilePath)
|
||||
this.currentLine = this.currentLine.slice(endOfFilePath)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
postProcess(data) {
|
||||
const all = []
|
||||
const errorsByLevel = {
|
||||
error: [],
|
||||
warning: [],
|
||||
typesetting: [],
|
||||
}
|
||||
const hashes = new Set()
|
||||
|
||||
const hashEntry = entry => entry.raw
|
||||
|
||||
data.forEach(item => {
|
||||
const hash = hashEntry(item)
|
||||
|
||||
if (this.ignoreDuplicates && hashes.has(hash)) {
|
||||
return
|
||||
}
|
||||
|
||||
errorsByLevel[item.level]?.push(item)
|
||||
|
||||
all.push(item)
|
||||
hashes.add(hash)
|
||||
})
|
||||
|
||||
return {
|
||||
errors: errorsByLevel.error,
|
||||
warnings: errorsByLevel.warning,
|
||||
typesetting: errorsByLevel.typesetting,
|
||||
all,
|
||||
files: this.rootFileList,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogText {
|
||||
constructor(text) {
|
||||
this.text = text.replace(/(\r\n)|\r/g, '\n')
|
||||
// Join any lines which look like they have wrapped.
|
||||
const wrappedLines = this.text.split('\n')
|
||||
this.lines = [wrappedLines[0]]
|
||||
|
||||
for (let i = 1; i < wrappedLines.length; i++) {
|
||||
// If the previous line is as long as the wrap limit then
|
||||
// append this line to it.
|
||||
// Some lines end with ... when LaTeX knows it's hit the limit
|
||||
// These shouldn't be wrapped.
|
||||
// If the next line looks like it could be an error (i.e. start with a !),
|
||||
// do not unwrap the line.
|
||||
const prevLine = wrappedLines[i - 1]
|
||||
const currentLine = wrappedLines[i]
|
||||
|
||||
if (
|
||||
prevLine.length === LOG_WRAP_LIMIT &&
|
||||
prevLine.slice(-3) !== '...' &&
|
||||
currentLine.charAt(0) !== '!'
|
||||
) {
|
||||
this.lines[this.lines.length - 1] += currentLine
|
||||
} else {
|
||||
this.lines.push(currentLine)
|
||||
}
|
||||
}
|
||||
this.row = 0
|
||||
}
|
||||
|
||||
nextLine() {
|
||||
this.row++
|
||||
if (this.row >= this.lines.length) {
|
||||
return false
|
||||
} else {
|
||||
return this.lines[this.row]
|
||||
}
|
||||
}
|
||||
|
||||
rewindLine() {
|
||||
this.row--
|
||||
}
|
||||
|
||||
linesUpToNextWhitespaceLine(stopAtError) {
|
||||
return this.linesUpToNextMatchingLine(/^ *$/, stopAtError)
|
||||
}
|
||||
|
||||
linesUpToNextMatchingLine(match, stopAtError) {
|
||||
const lines = []
|
||||
|
||||
while (true) {
|
||||
const nextLine = this.nextLine()
|
||||
|
||||
if (nextLine === false) {
|
||||
break
|
||||
}
|
||||
|
||||
if (stopAtError && nextLine.match(/^! /)) {
|
||||
this.rewindLine()
|
||||
break
|
||||
}
|
||||
|
||||
lines.push(nextLine)
|
||||
|
||||
if (nextLine.match(match)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user