kindwolf.org Git repositories codemirror-mode-ipaddr / master src / ipaddr.js
master

Tree @master (Download .tar.gz)

ipaddr.js @masterraw · history · blame

CodeMirror.defineMode('ipaddr', function(editor_options, mode_options) {
	var
		UNKNOWN = 0,
		IPV4 = 1, // Pure IPv4 address
		IPV6 = 2, // Pure IPv6 address
		DUAL = 3; // Dual IPv6+IPv4 address, e.g. 2001:db8::1234:5678:5.6.7.8

	// Default settings:
	var options = {
		ipv4_ranges: true, // Accept IPv4 ranges, e.g. 10.0.0.0-10.255.255.255
		ipv6_ranges: true, // Accept IPv6 ranges, e.g. 2001:0db8::-2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff
		ipv4_cidr: true,   // Accept IPv4 CIDR, e.g. 10.0.0.0/8
		ipv6_cidr: true,   // Accept IPv6 CIDR, e.g. 2001:db8::/32
		strict: true,      // Strict mode: accept only one address or range (relevant as a nested mode)
		default_state: UNKNOWN, // default state; best set through a MIME type
	};

	// Override options.default_state if invoked through a MIME type:
	function is_mime_type(str) { return (typeof str === 'string') && (str.indexOf('/') !== -1); }
	var invoked_mime;
	if (is_mime_type(mode_options.name)) invoked_mime = mode_options.name;
	else if (is_mime_type(editor_options.mode)) invoked_mime = editor_options.mode;
	else if (is_mime_type(editor_options.mode.name)) invoked_mime = editor_options.mode.name;
	if (invoked_mime === 'text/x-ipv4-address') options.default_state = IPV4;
	else if (invoked_mime === 'text/x-ipv6-address') options.default_state = IPV6;

	// Override default settings with user-provided settings:
	for (option in options) {
		if (option in mode_options) options[option] = mode_options[option];
	}

	// Helpers:
	ipv4_re = new RegExp('^(?:\\d+\\.){1,3}\\d*');
	function error(stream, state) {
		if (options.strict) state.mark_as_error = true;
		reset(state);
		if (!stream.current()) stream.next();
		return 'error';
	}
	function guess_mode(stream, state) {
		var rem;
		// Determine whether we should expect an IPv4 or IPv6 address based on
		// the first encountered separator:
		if (rem = stream.match(/^[^.:]*([.:])/, false)) {
			reset(state);
			state.current_mode = rem[1] === '.' ? IPV4 : IPV6;
		}
	}
	function ipv6_accept_trailing_ipv4(state) {
		// An IPv6 address has up to 8 blocks.
		// A trailing IPv4 is 32 bits, i.e. 2 blocks.
		// A double-colon represents at least 2 blocks.
		// Consequently:
		// if the current IP address features a double-colon *and*
		// a trailing IPv4, it cannot have more than 4 blocks (8 - 2 - 2):
		if (state.double_colon) return state.block_count <= 4;
		// if the current IP address does not feature a double-colon but
		// features a trailing IPv4, then it must have exactly 6 blocks (8 - 2):
		return state.block_count === 6;
	}
	function ipv6_enough_bits(state) {
		return state.double_colon ? true : (state.block_count === 8);
	}
	function ipv6_max_blocks(state) {
		return state.double_colon ? 6 : 8;
	}

	// Handlers:
	function handle_cidr_suffix(stream, state) {
		if (state.range) return false;
		var rem;
		if (!state.cidr) {
			if (stream.eat('/')) {
				state.cidr = true;
				return 'meta';
			}
			return false;
		} else if (rem = stream.match(/^(\d+)/)) {
			if (Number(rem[1]) <= (state.current_mode === IPV4 ? 32 : 128)) {
				if (options.strict) state.mark_as_error = true;
				reset(state);
				return 'attribute';
			}
		}
		return error(stream, state);
	}

	function handle_range(stream, state) {
		if (!state.range && stream.eat('-')) {
			var new_mode = (state.current_mode === IPV4) ? IPV4 : IPV6;
			reset(state);
			// Ensure the second address is the same type as the first one:
			state.current_mode = new_mode;
			state.range = true;
			return 'attribute';
		}
		return false;
	}

	function handle_ipv4(stream, state) {
		if (state.block_count === 4) {
			var ret;
			if (options.ipv4_cidr) {
				if (ret = handle_cidr_suffix(stream, state)) return ret;
			}
			if (options.ipv4_ranges) {
				if (ret = handle_range(stream, state)) return ret;
			}
			// All done:
			reset(state);
			if (options.strict) state.mark_as_error = true;
		}
		if (state.expect_dot && stream.match('.')) {
			state.expect_dot = false;
			return 'punctuation';
		}
		if (rem = stream.match(/^(\d+)/)) {
			if (Number(rem[1]) < 256) {
				++ state.block_count;
				state.expect_dot = state.block_count < 4;
				return 'number';
			}
		}
		return error(stream, state);
	}


	function handle_ipv6(stream, state) {
		if (ipv6_enough_bits(state)) {
			var ret;
			if (options.ipv6_cidr) {
				if (ret = handle_cidr_suffix(stream, state)) return ret;
			}
			if (options.ipv6_ranges) {
				if (ret = handle_range(stream, state)) return ret;
			}
		}
		if (state.expect_digits) {
			// Handle dual addresses:
			if (stream.match(ipv4_re, false)) {
				if (ipv6_accept_trailing_ipv4(state)) {
					reset(state);
					state.current_mode = DUAL;
					return tokenBase(stream, state);
				}
				stream.match(ipv4_re);
				return 'error';
			}
			else if (stream.match(/^([0-9a-fA-F]{1,4})/)) {
				++ state.block_count;
				state.expect_colon = state.block_count < ipv6_max_blocks(state);
				state.expect_double_colon = !state.double_colon && state.block_count <= 6;
				state.expect_digits = false;
				return 'number';
			}
		}
		if (state.expect_double_colon && stream.match('::')) {
			state.double_colon = true; // mark the use of a double-colon
			state.expect_colon = false;
			state.expect_double_colon = false;
			state.expect_digits = state.block_count < 6;
			return 'punctuation';
		}
		if (state.expect_colon && stream.eat(':')) {
			state.expect_colon = false;
			state.expect_double_colon = false;
			state.expect_digits = state.block_count < ipv6_max_blocks(state);
			return 'punctuation';
		}
		return error(stream, state);
	}

	function tokenBase(stream, state) {
		if (stream.sol() && !state.just_begun) { // 2nd line or more
			if (options.strict) state.mark_as_error = true; // forbidden in strict mode
			reset(state);
		}
		if (state.just_begun) delete state.just_begun;
		if (state.mark_as_error) return error(stream, state);
		if (state.cidr) return handle_cidr_suffix(stream, state);
		if (state.current_mode === UNKNOWN) guess_mode(stream, state);
		if (state.current_mode === IPV6) return handle_ipv6(stream, state);
		if (state.current_mode & IPV4) return handle_ipv4(stream, state);
		return error(stream, state);
	}

	function reset(state) {
		state.current_mode = options.default_state;
		state.block_count = 0;
		state.range = state.cidr = false;
		// IPv4-specific:
		state.expect_dot = false;
		// IPv6-specific:
		state.expect_digits = state.expect_double_colon = true;
		state.expect_colon = state.double_colon = false;
		return state;
	}

	return {
		startState: function() { return reset({just_begun: true}); },
		token: tokenBase,
	};
});

CodeMirror.defineMIME('text/x-ip-address',   'ipaddr');
CodeMirror.defineMIME('text/x-ipv4-address', 'ipaddr');
CodeMirror.defineMIME('text/x-ipv6-address', 'ipaddr');